From 34dcc1914d1a1221ee872525e8963d5891437272 Mon Sep 17 00:00:00 2001 From: Marta Motyczynska Date: Mon, 22 Apr 2024 09:29:08 +0200 Subject: [PATCH 001/256] Add license key basic ckeck. --- packages/ckeditor5-core/src/editor/editor.ts | 80 +++++++++++++++++++ .../ckeditor5-core/tests/editor/editor.js | 69 +++++++++++++++- 2 files changed, 148 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5-core/src/editor/editor.ts b/packages/ckeditor5-core/src/editor/editor.ts index 2b914f6bcd5..be3bbb120c0 100644 --- a/packages/ckeditor5-core/src/editor/editor.ts +++ b/packages/ckeditor5-core/src/editor/editor.ts @@ -258,6 +258,11 @@ export default abstract class Editor extends ObservableMixin() { */ public abstract get ui(): EditorUI; + /** + * the license key payload. + */ + public licensePayload: any; + /** * The editor context. * When it is not provided through the configuration, the editor creates it. @@ -330,6 +335,8 @@ export default abstract class Editor extends ObservableMixin() { this.conversion.addAlias( 'dataDowncast', this.data.downcastDispatcher ); this.conversion.addAlias( 'editingDowncast', this.editing.downcastDispatcher ); + this._verifyLicenseKey(); + this.keystrokes = new EditingKeystrokeHandler( this ); this.keystrokes.listenTo( this.editing.view.document ); @@ -642,6 +649,79 @@ export default abstract class Editor extends ObservableMixin() { public static create( ...args: Array ): void { // eslint-disable-line @typescript-eslint/no-unused-vars throw new Error( 'This is an abstract method.' ); } + + /** + * Performs basic license key check. Enables the editor's read-only mode if the license key's validation period has expired + * or the license key format is incorrect. + */ + private _verifyLicenseKey() { + const licenseKey = this.config.get( 'licenseKey' ); + + if ( !licenseKey ) { + // TODO: For now, we don't block the editor if a licence key is not provided. + return; + } + + const encodedPayload = getPayload( licenseKey ); + + if ( !encodedPayload ) { + blockEditorWrongLicenseFormat( this ); + + return; + } + + this.licensePayload = parseBase64EncodedObject( encodedPayload ); + + if ( !this.licensePayload ) { + blockEditorWrongLicenseFormat( this ); + + return; + } + + if ( !this.licensePayload.exp ) { + blockEditorWrongLicenseFormat( this ); + + return; + } + + const expirationDate = new Date( this.licensePayload.exp * 1000 ); + + if ( expirationDate < new Date() ) { + this.enableReadOnlyMode( 'licenseExpired' ); + + console.warn( 'The validation period for the editor license key has expired.' ); + } + + function getPayload( licenseKey: string ): string | null { + const parts = licenseKey.split( '.' ); + + if ( parts.length != 3 ) { + return null; + } + + return parts[ 1 ]; + } + + function parseBase64EncodedObject( encoded: string ): Record | null { + try { + if ( !encoded.startsWith( 'ey' ) ) { + return null; + } + + const decoded = atob( encoded.replace( /-/g, '+' ).replace( /_/g, '/' ) ); + + return JSON.parse( decoded ); + } catch ( e ) { + return null; + } + } + + function blockEditorWrongLicenseFormat( editor: Editor ) { + console.warn( 'The format of the license key is invalid.' ); + + editor.enableReadOnlyMode( 'licenseFormatInvalid' ); + } + } } /** diff --git a/packages/ckeditor5-core/tests/editor/editor.js b/packages/ckeditor5-core/tests/editor/editor.js index 4882f449c00..cc0d14def55 100644 --- a/packages/ckeditor5-core/tests/editor/editor.js +++ b/packages/ckeditor5-core/tests/editor/editor.js @@ -3,7 +3,7 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ -/* globals document, window, setTimeout */ +/* globals document, window, setTimeout, console */ import Editor from '../../src/editor/editor.js'; import Context from '../../src/context.js'; @@ -215,6 +215,73 @@ describe( 'Editor', () => { sinon.assert.calledWith( spy, editor.editing.view.document ); } ); + + describe( 'license key verification', () => { + let stub; + + beforeEach( () => { + stub = testUtils.sinon.stub( console, 'warn' ); + } ); + + it( 'should not block the editor when the license key is valid (expiration date in the future)', () => { + // eslint-disable-next-line max-len + const licenseKey = 'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjIyMDg5ODg4MDAsImp0aSI6ImZjYjE0ZDAwLTJmZmItNDQxMy1iMzM3LTljMjhiOTE0MjRjMCIsImxpY2Vuc2VUeXBlIjoidHJpYWwifQ.eyOjMnJ3Ni4RMaDwe6UhAPiyPQLS5FtqKHOh6TEgnoVRH_y6-Gfg_sP7OnLBSwfHOQ-sz9kIgg6CWpRJBlTAWQ'; + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.notCalled( stub ); + expect( editor.isReadOnly ).to.be.false; + } ); + + it( 'should block the editor when the license key is not valid (expiration date in the past)', () => { + // eslint-disable-next-line max-len + const licenseKey = 'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MDQwNjcyMDAsImp0aSI6IjZlY2JkZjU2LTVlYjMtNGIyYy05NWI1LWU5M2MwZDZiNmZmMSIsImxpY2Vuc2VUeXBlIjoidHJpYWwifQ.7UCzTCqA0HZWMejOo5mGkP9hFE623mkFDnQ6PS3B7NJhROztZUPoddZdECsgBIyAznF3_5kimfLcDALnwaUa8A'; + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.calledWithMatch( stub, 'The validation period for the editor license key has expired.' ); + expect( editor.isReadOnly ).to.be.true; + } ); + + it( 'should block the editor when the license key has wrong format (missing header part)', () => { + // eslint-disable-next-line max-len + const licenseKey = 'eyJleHAiOjIyMDg5ODg4MDAsImp0aSI6ImZjYjE0ZDAwLTJmZmItNDQxMy1iMzM3LTljMjhiOTE0MjRjMCIsImxpY2Vuc2VUeXBlIjoidHJpYWwifQ.eyOjMnJ3Ni4RMaDwe6UhAPiyPQLS5FtqKHOh6TEgnoVRH_y6-Gfg_sP7OnLBSwfHOQ-sz9kIgg6CWpRJBlTAWQ'; + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.calledWithMatch( stub, 'The format of the license key is invalid.' ); + expect( editor.isReadOnly ).to.be.true; + } ); + + it( 'should block the editor when the license key has wrong format (payload does not start with `ey`)', () => { + // eslint-disable-next-line max-len + const licenseKey = 'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.JleHAiOjIyMDg5ODg4MDAsImp0aSI6ImZjYjE0ZDAwLTJmZmItNDQxMy1iMzM3LTljMjhiOTE0MjRjMCIsImxpY2Vuc2VUeXBlIjoidHJpYWwifQ.eyOjMnJ3Ni4RMaDwe6UhAPiyPQLS5FtqKHOh6TEgnoVRH_y6-Gfg_sP7OnLBSwfHOQ-sz9kIgg6CWpRJBlTAWQ'; + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.calledWithMatch( stub, 'The format of the license key is invalid.' ); + expect( editor.isReadOnly ).to.be.true; + } ); + + it( 'should block the editor when the license key has wrong format (payload not parsable as a JSON object)', () => { + // eslint-disable-next-line max-len + const licenseKey = 'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyZm9v.eyOjMnJ3Ni4RMaDwe6UhAPiyPQLS5FtqKHOh6TEgnoVRH_y6-Gfg_sP7OnLBSwfHOQ-sz9kIgg6CWpRJBlTAWQ'; + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.calledWithMatch( stub, 'The format of the license key is invalid.' ); + expect( editor.isReadOnly ).to.be.true; + } ); + + it( 'should block the editor when the license key has wrong format (missing expiration date)', () => { + // eslint-disable-next-line max-len + const licenseKey = 'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiI1ZjY3ODk5YS04OWQ4LTQxYWUtOWU4Yi1mMzhiMTIzZjI3YjYifQ.2K6tEH9JQaKDt5WsVZgamwfVNTTg2VxHRllEp0Vz5c5CNfqG_SrZA5hxSRdCF__ZLWIgaQfCH6jpug5YPI1ZFA'; + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.calledWithMatch( stub, 'The format of the license key is invalid.' ); + expect( editor.isReadOnly ).to.be.true; + } ); + } ); } ); describe( 'context integration', () => { From 4b912fc87aec77103ca9c004262a1ab7db07992d Mon Sep 17 00:00:00 2001 From: Marta Motyczynska Date: Mon, 22 Apr 2024 11:43:49 +0200 Subject: [PATCH 002/256] Move parseBase64EncodedObject to utils and add tests. Simplify license keys in test. Use release date. --- packages/ckeditor5-core/src/editor/editor.ts | 31 +++++-------------- .../ckeditor5-core/tests/editor/editor.js | 12 +++---- packages/ckeditor5-utils/src/index.ts | 1 + .../src/parsebase64encodedobject.ts | 25 +++++++++++++++ .../tests/parsebase64encodedobject.js | 30 ++++++++++++++++++ 5 files changed, 69 insertions(+), 30 deletions(-) create mode 100644 packages/ckeditor5-utils/src/parsebase64encodedobject.ts create mode 100644 packages/ckeditor5-utils/tests/parsebase64encodedobject.js diff --git a/packages/ckeditor5-core/src/editor/editor.ts b/packages/ckeditor5-core/src/editor/editor.ts index be3bbb120c0..29b758509d6 100644 --- a/packages/ckeditor5-core/src/editor/editor.ts +++ b/packages/ckeditor5-core/src/editor/editor.ts @@ -11,6 +11,8 @@ import { Config, CKEditorError, ObservableMixin, + parseBase64EncodedObject, + releaseDate, type Locale, type LocaleTranslate, type ObservableChangeEvent @@ -258,11 +260,6 @@ export default abstract class Editor extends ObservableMixin() { */ public abstract get ui(): EditorUI; - /** - * the license key payload. - */ - public licensePayload: any; - /** * The editor context. * When it is not provided through the configuration, the editor creates it. @@ -670,23 +667,23 @@ export default abstract class Editor extends ObservableMixin() { return; } - this.licensePayload = parseBase64EncodedObject( encodedPayload ); + const licensePayload = parseBase64EncodedObject( encodedPayload ); - if ( !this.licensePayload ) { + if ( !licensePayload ) { blockEditorWrongLicenseFormat( this ); return; } - if ( !this.licensePayload.exp ) { + if ( !licensePayload.exp ) { blockEditorWrongLicenseFormat( this ); return; } - const expirationDate = new Date( this.licensePayload.exp * 1000 ); + const expirationDate = new Date( licensePayload.exp * 1000 ); - if ( expirationDate < new Date() ) { + if ( expirationDate < releaseDate ) { this.enableReadOnlyMode( 'licenseExpired' ); console.warn( 'The validation period for the editor license key has expired.' ); @@ -702,20 +699,6 @@ export default abstract class Editor extends ObservableMixin() { return parts[ 1 ]; } - function parseBase64EncodedObject( encoded: string ): Record | null { - try { - if ( !encoded.startsWith( 'ey' ) ) { - return null; - } - - const decoded = atob( encoded.replace( /-/g, '+' ).replace( /_/g, '/' ) ); - - return JSON.parse( decoded ); - } catch ( e ) { - return null; - } - } - function blockEditorWrongLicenseFormat( editor: Editor ) { console.warn( 'The format of the license key is invalid.' ); diff --git a/packages/ckeditor5-core/tests/editor/editor.js b/packages/ckeditor5-core/tests/editor/editor.js index cc0d14def55..b9305280427 100644 --- a/packages/ckeditor5-core/tests/editor/editor.js +++ b/packages/ckeditor5-core/tests/editor/editor.js @@ -225,7 +225,7 @@ describe( 'Editor', () => { it( 'should not block the editor when the license key is valid (expiration date in the future)', () => { // eslint-disable-next-line max-len - const licenseKey = 'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjIyMDg5ODg4MDAsImp0aSI6ImZjYjE0ZDAwLTJmZmItNDQxMy1iMzM3LTljMjhiOTE0MjRjMCIsImxpY2Vuc2VUeXBlIjoidHJpYWwifQ.eyOjMnJ3Ni4RMaDwe6UhAPiyPQLS5FtqKHOh6TEgnoVRH_y6-Gfg_sP7OnLBSwfHOQ-sz9kIgg6CWpRJBlTAWQ'; + const licenseKey = 'foo.eyJleHAiOjIyMDg5ODg4MDAsImp0aSI6ImZjYjE0ZDAwLTJmZmItNDQxMy1iMzM3LTljMjhiOTE0MjRjMCIsImxpY2Vuc2VUeXBlIjoidHJpYWwifQ.bar'; const editor = new TestEditor( { licenseKey } ); @@ -235,7 +235,7 @@ describe( 'Editor', () => { it( 'should block the editor when the license key is not valid (expiration date in the past)', () => { // eslint-disable-next-line max-len - const licenseKey = 'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MDQwNjcyMDAsImp0aSI6IjZlY2JkZjU2LTVlYjMtNGIyYy05NWI1LWU5M2MwZDZiNmZmMSIsImxpY2Vuc2VUeXBlIjoidHJpYWwifQ.7UCzTCqA0HZWMejOo5mGkP9hFE623mkFDnQ6PS3B7NJhROztZUPoddZdECsgBIyAznF3_5kimfLcDALnwaUa8A'; + const licenseKey = 'foo.eyJleHAiOjE3MDQwNjcyMDAsImp0aSI6IjZlY2JkZjU2LTVlYjMtNGIyYy05NWI1LWU5M2MwZDZiNmZmMSIsImxpY2Vuc2VUeXBlIjoidHJpYWwifQ.bar'; const editor = new TestEditor( { licenseKey } ); @@ -245,7 +245,7 @@ describe( 'Editor', () => { it( 'should block the editor when the license key has wrong format (missing header part)', () => { // eslint-disable-next-line max-len - const licenseKey = 'eyJleHAiOjIyMDg5ODg4MDAsImp0aSI6ImZjYjE0ZDAwLTJmZmItNDQxMy1iMzM3LTljMjhiOTE0MjRjMCIsImxpY2Vuc2VUeXBlIjoidHJpYWwifQ.eyOjMnJ3Ni4RMaDwe6UhAPiyPQLS5FtqKHOh6TEgnoVRH_y6-Gfg_sP7OnLBSwfHOQ-sz9kIgg6CWpRJBlTAWQ'; + const licenseKey = 'eyJleHAiOjIyMDg5ODg4MDAsImp0aSI6ImZjYjE0ZDAwLTJmZmItNDQxMy1iMzM3LTljMjhiOTE0MjRjMCIsImxpY2Vuc2VUeXBlIjoidHJpYWwifQ.bar'; const editor = new TestEditor( { licenseKey } ); @@ -255,7 +255,7 @@ describe( 'Editor', () => { it( 'should block the editor when the license key has wrong format (payload does not start with `ey`)', () => { // eslint-disable-next-line max-len - const licenseKey = 'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.JleHAiOjIyMDg5ODg4MDAsImp0aSI6ImZjYjE0ZDAwLTJmZmItNDQxMy1iMzM3LTljMjhiOTE0MjRjMCIsImxpY2Vuc2VUeXBlIjoidHJpYWwifQ.eyOjMnJ3Ni4RMaDwe6UhAPiyPQLS5FtqKHOh6TEgnoVRH_y6-Gfg_sP7OnLBSwfHOQ-sz9kIgg6CWpRJBlTAWQ'; + const licenseKey = 'foo.JleHAiOjIyMDg5ODg4MDAsImp0aSI6ImZjYjE0ZDAwLTJmZmItNDQxMy1iMzM3LTljMjhiOTE0MjRjMCIsImxpY2Vuc2VUeXBlIjoidHJpYWwifQ.bar'; const editor = new TestEditor( { licenseKey } ); @@ -265,7 +265,7 @@ describe( 'Editor', () => { it( 'should block the editor when the license key has wrong format (payload not parsable as a JSON object)', () => { // eslint-disable-next-line max-len - const licenseKey = 'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyZm9v.eyOjMnJ3Ni4RMaDwe6UhAPiyPQLS5FtqKHOh6TEgnoVRH_y6-Gfg_sP7OnLBSwfHOQ-sz9kIgg6CWpRJBlTAWQ'; + const licenseKey = 'foo.eyZm9v.bar'; const editor = new TestEditor( { licenseKey } ); @@ -275,7 +275,7 @@ describe( 'Editor', () => { it( 'should block the editor when the license key has wrong format (missing expiration date)', () => { // eslint-disable-next-line max-len - const licenseKey = 'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiI1ZjY3ODk5YS04OWQ4LTQxYWUtOWU4Yi1mMzhiMTIzZjI3YjYifQ.2K6tEH9JQaKDt5WsVZgamwfVNTTg2VxHRllEp0Vz5c5CNfqG_SrZA5hxSRdCF__ZLWIgaQfCH6jpug5YPI1ZFA'; + const licenseKey = 'foo.eyJqdGkiOiI1ZjY3ODk5YS04OWQ4LTQxYWUtOWU4Yi1mMzhiMTIzZjI3YjYifQ.bar'; const editor = new TestEditor( { licenseKey } ); sinon.assert.calledWithMatch( stub, 'The format of the license key is invalid.' ); diff --git a/packages/ckeditor5-utils/src/index.ts b/packages/ckeditor5-utils/src/index.ts index 2e49dc415ff..08c271e0be6 100644 --- a/packages/ckeditor5-utils/src/index.ts +++ b/packages/ckeditor5-utils/src/index.ts @@ -93,6 +93,7 @@ export { default as uid } from './uid.js'; export { default as delay, type DelayedFunc } from './delay.js'; export { default as verifyLicense } from './verifylicense.js'; export { default as wait } from './wait.js'; +export { default as parseBase64EncodedObject } from './parsebase64encodedobject.js'; export * from './unicode.js'; diff --git a/packages/ckeditor5-utils/src/parsebase64encodedobject.ts b/packages/ckeditor5-utils/src/parsebase64encodedobject.ts new file mode 100644 index 00000000000..a3ed488462a --- /dev/null +++ b/packages/ckeditor5-utils/src/parsebase64encodedobject.ts @@ -0,0 +1,25 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/** + * @module utils/parsebase64encodedobject + */ + +/** + * Parses a base64-encoded object and returns the decoded object, or null if the decoding was unsuccessful. + */ +export default function parseBase64EncodedObject( encoded: string ): Record | null { + try { + if ( !encoded.startsWith( 'ey' ) ) { + return null; + } + + const decoded = atob( encoded.replace( /-/g, '+' ).replace( /_/g, '/' ) ); + + return JSON.parse( decoded ); + } catch ( e ) { + return null; + } +} diff --git a/packages/ckeditor5-utils/tests/parsebase64encodedobject.js b/packages/ckeditor5-utils/tests/parsebase64encodedobject.js new file mode 100644 index 00000000000..e3865412d54 --- /dev/null +++ b/packages/ckeditor5-utils/tests/parsebase64encodedobject.js @@ -0,0 +1,30 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +import parseBase64EncodedObject from '../src/parsebase64encodedobject.js'; + +/* globals btoa */ + +describe( 'parseBase64EncodedObject', () => { + it( 'should return a decoded object', () => { + const obj = { foo: 1 }; + const encoded = btoa( JSON.stringify( obj ) ); + + expect( parseBase64EncodedObject( encoded ) ).to.deep.equal( obj ); + } ); + + it( 'should return null if it is not an object', () => { + const str = 'foo'; + const encoded = btoa( JSON.stringify( str ) ); + + expect( parseBase64EncodedObject( encoded ) ).to.be.null; + } ); + + it( 'should return null of it is not parsable', () => { + const encoded = btoa( '{"foo":1' ); + + expect( parseBase64EncodedObject( encoded ) ).to.be.null; + } ); +} ); From 9cf2fdbc0206e4b4c28f650336b97641cb926731 Mon Sep 17 00:00:00 2001 From: Marta Motyczynska Date: Mon, 22 Apr 2024 11:50:58 +0200 Subject: [PATCH 003/256] Refactor function for blocking editor (add reason and message). --- packages/ckeditor5-core/src/editor/editor.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/ckeditor5-core/src/editor/editor.ts b/packages/ckeditor5-core/src/editor/editor.ts index 29b758509d6..9126984a4ce 100644 --- a/packages/ckeditor5-core/src/editor/editor.ts +++ b/packages/ckeditor5-core/src/editor/editor.ts @@ -662,7 +662,7 @@ export default abstract class Editor extends ObservableMixin() { const encodedPayload = getPayload( licenseKey ); if ( !encodedPayload ) { - blockEditorWrongLicenseFormat( this ); + blockEditor( this, 'licenseFormatInvalid', 'The format of the license key is invalid.' ); return; } @@ -670,13 +670,13 @@ export default abstract class Editor extends ObservableMixin() { const licensePayload = parseBase64EncodedObject( encodedPayload ); if ( !licensePayload ) { - blockEditorWrongLicenseFormat( this ); + blockEditor( this, 'licenseFormatInvalid', 'The format of the license key is invalid.' ); return; } if ( !licensePayload.exp ) { - blockEditorWrongLicenseFormat( this ); + blockEditor( this, 'licenseFormatInvalid', 'The format of the license key is invalid.' ); return; } @@ -684,9 +684,7 @@ export default abstract class Editor extends ObservableMixin() { const expirationDate = new Date( licensePayload.exp * 1000 ); if ( expirationDate < releaseDate ) { - this.enableReadOnlyMode( 'licenseExpired' ); - - console.warn( 'The validation period for the editor license key has expired.' ); + blockEditor( this, 'licenseExpired', 'The validation period for the editor license key has expired.' ); } function getPayload( licenseKey: string ): string | null { @@ -699,10 +697,10 @@ export default abstract class Editor extends ObservableMixin() { return parts[ 1 ]; } - function blockEditorWrongLicenseFormat( editor: Editor ) { - console.warn( 'The format of the license key is invalid.' ); + function blockEditor( editor: Editor, reason: string, message: string ) { + console.warn( message ); - editor.enableReadOnlyMode( 'licenseFormatInvalid' ); + editor.enableReadOnlyMode( reason ); } } } From 2f0f43ff302079a4beeff6081e24008d891c074b Mon Sep 17 00:00:00 2001 From: Arkadiusz Filipczak Date: Mon, 22 Apr 2024 16:32:03 +0200 Subject: [PATCH 004/256] Send usage request. --- packages/ckeditor5-core/src/editor/editor.ts | 51 ++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/packages/ckeditor5-core/src/editor/editor.ts b/packages/ckeditor5-core/src/editor/editor.ts index 9126984a4ce..8ecd5dfb2aa 100644 --- a/packages/ckeditor5-core/src/editor/editor.ts +++ b/packages/ckeditor5-core/src/editor/editor.ts @@ -13,6 +13,7 @@ import { ObservableMixin, parseBase64EncodedObject, releaseDate, + uid, type Locale, type LocaleTranslate, type ObservableChangeEvent @@ -647,6 +648,12 @@ export default abstract class Editor extends ObservableMixin() { throw new Error( 'This is an abstract method.' ); } + private _getTelemetryData() { + return { + editorVersion: globalThis.CKEDITOR_VERSION + }; + } + /** * Performs basic license key check. Enables the editor's read-only mode if the license key's validation period has expired * or the license key format is incorrect. @@ -687,6 +694,25 @@ export default abstract class Editor extends ObservableMixin() { blockEditor( this, 'licenseExpired', 'The validation period for the editor license key has expired.' ); } + if ( licensePayload.usageEndpoint ) { + this.once( 'ready', () => { + const telemetryData = this._getTelemetryData(); + + this._sendUsageRequest( licensePayload.usageEndpoint, licenseKey, telemetryData ).then( response => { + const { status, message } = response; + + if ( message ) { + console.warn( message ); + } + + if ( status != 'ok' ) { + // TODO: check if this message is ok here. + blockEditor( this, 'usageExceeded', 'The licensed usage count exceeded' ); + } + } ); + }, { priority: 'high' } ); + } + function getPayload( licenseKey: string ): string | null { const parts = licenseKey.split( '.' ); @@ -703,6 +729,31 @@ export default abstract class Editor extends ObservableMixin() { editor.enableReadOnlyMode( reason ); } } + + private async _sendUsageRequest( + endpoint: string, + licenseKey: string, + telemetry: Record + ) { + const request = { + requestId: uid(), + requestTime: Math.round( Date.now() / 1000 ), + license: licenseKey, + telemetry + }; + + const response = await fetch( new URL( endpoint ), { + method: 'POST', + body: JSON.stringify( request ) + } ); + + if ( !response.ok ) { + // TODO: refine message. + throw new Error( `HTTP Response: ${ response.status }` ); + } + + return response.json(); + } } /** From 7d536ff142ae73178d6fedd9fe36e54478a37ac1 Mon Sep 17 00:00:00 2001 From: Arkadiusz Filipczak Date: Tue, 23 Apr 2024 16:12:00 +0200 Subject: [PATCH 005/256] PoweredBy updated to use new license key. --- .../ckeditor5-ui/src/editorui/poweredby.ts | 14 ++- .../ckeditor5-ui/tests/editorui/poweredby.js | 31 +++-- packages/ckeditor5-utils/src/index.ts | 1 - packages/ckeditor5-utils/src/verifylicense.ts | 108 ----------------- .../ckeditor5-utils/tests/verifylicense.js | 114 ------------------ 5 files changed, 30 insertions(+), 238 deletions(-) delete mode 100644 packages/ckeditor5-utils/src/verifylicense.ts delete mode 100644 packages/ckeditor5-utils/tests/verifylicense.js diff --git a/packages/ckeditor5-ui/src/editorui/poweredby.ts b/packages/ckeditor5-ui/src/editorui/poweredby.ts index 283b3b52f31..7b858d2e0cd 100644 --- a/packages/ckeditor5-ui/src/editorui/poweredby.ts +++ b/packages/ckeditor5-ui/src/editorui/poweredby.ts @@ -11,7 +11,7 @@ import type { Editor, UiConfig } from '@ckeditor/ckeditor5-core'; import { DomEmitterMixin, Rect, - verifyLicense, + parseBase64EncodedObject, type PositionOptions, type Locale } from '@ckeditor/ckeditor5-utils'; @@ -100,11 +100,15 @@ export default class PoweredBy extends DomEmitterMixin() { */ private _handleEditorReady(): void { const editor = this.editor; - const forceVisible = !!editor.config.get( 'ui.poweredBy.forceVisible' ); + const forceVisible = editor.config.get( 'ui.poweredBy.forceVisible' ); - /* istanbul ignore next -- @preserve */ - if ( !forceVisible && verifyLicense( editor.config.get( 'licenseKey' ) ) === 'VALID' ) { - return; + if ( !forceVisible ) { + const licenseKey = editor.config.get( 'licenseKey' ); + const licenseContent = licenseKey && parseBase64EncodedObject( licenseKey.split( '.' )[ 1 ] ); + + if ( licenseContent && licenseContent.whiteLabel ) { + return; + } } // No view means no body collection to append the powered by balloon to. diff --git a/packages/ckeditor5-ui/tests/editorui/poweredby.js b/packages/ckeditor5-ui/tests/editorui/poweredby.js index fdcee1ccae4..35ea23bcf39 100644 --- a/packages/ckeditor5-ui/tests/editorui/poweredby.js +++ b/packages/ckeditor5-ui/tests/editorui/poweredby.js @@ -83,12 +83,9 @@ describe( 'PoweredBy', () => { expect( editor.ui.poweredBy._balloonView ).to.be.instanceOf( BalloonPanelView ); } ); - it( 'should not create the balloon when a valid license key is configured', async () => { + it( 'should not create the balloon when a white-label license key is configured', async () => { const editor = await createEditor( element, { - // eslint-disable-next-line max-len - // https://github.com/ckeditor/ckeditor5/blob/226bf243d1eb8bae2d447f631d6f5d9961bc6541/packages/ckeditor5-utils/tests/verifylicense.js#L14 - // eslint-disable-next-line max-len - licenseKey: 'dG9vZWFzZXRtcHNsaXVyb3JsbWlkbXRvb2Vhc2V0bXBzbGl1cm9ybG1pZG10b29lYXNldG1wc2xpdXJvcmxtaWRtLU1qQTBOREEyTVRJPQ==' + licenseKey: 'foo.eyJ3aGl0ZUxhYmVsIjp0cnVlfQ.bar' } ); expect( editor.ui.poweredBy._balloonView ).to.be.null; @@ -100,12 +97,9 @@ describe( 'PoweredBy', () => { await editor.destroy(); } ); - it( 'should create the balloon when a valid license key is configured and `forceVisible` is set to true', async () => { + it( 'should create the balloon when a white-label license key is configured and `forceVisible` is set to true', async () => { const editor = await createEditor( element, { - // eslint-disable-next-line max-len - // https://github.com/ckeditor/ckeditor5/blob/226bf243d1eb8bae2d447f631d6f5d9961bc6541/packages/ckeditor5-utils/tests/verifylicense.js#L14 - // eslint-disable-next-line max-len - licenseKey: 'dG9vZWFzZXRtcHNsaXVyb3JsbWlkbXRvb2Vhc2V0bXBzbGl1cm9ybG1pZG10b29lYXNldG1wc2xpdXJvcmxtaWRtLU1qQTBOREEyTVRJPQ==', + licenseKey: 'foo.eyJ3aGl0ZUxhYmVsIjp0cnVlfQ.bar', ui: { poweredBy: { forceVisible: true @@ -121,6 +115,23 @@ describe( 'PoweredBy', () => { await editor.destroy(); } ); + + it( 'should create the balloon when a non-white-label license key is configured', async () => { + const editor = await createEditor( element, { + // eslint-disable-next-line max-len + // https://github.com/ckeditor/ckeditor5/blob/226bf243d1eb8bae2d447f631d6f5d9961bc6541/packages/ckeditor5-utils/tests/verifylicense.js#L14 + // eslint-disable-next-line max-len + licenseKey: 'foo.eyJhYmMiOjF9.bar' + } ); + + expect( editor.ui.poweredBy._balloonView ).to.be.null; + + focusEditor( editor ); + + expect( editor.ui.poweredBy._balloonView ).to.be.instanceOf( BalloonPanelView ); + + await editor.destroy(); + } ); } ); describe( 'balloon management on editor focus change', () => { diff --git a/packages/ckeditor5-utils/src/index.ts b/packages/ckeditor5-utils/src/index.ts index 08c271e0be6..391c7df49a5 100644 --- a/packages/ckeditor5-utils/src/index.ts +++ b/packages/ckeditor5-utils/src/index.ts @@ -91,7 +91,6 @@ export { default as spliceArray } from './splicearray.js'; export { default as uid } from './uid.js'; export { default as delay, type DelayedFunc } from './delay.js'; -export { default as verifyLicense } from './verifylicense.js'; export { default as wait } from './wait.js'; export { default as parseBase64EncodedObject } from './parsebase64encodedobject.js'; diff --git a/packages/ckeditor5-utils/src/verifylicense.ts b/packages/ckeditor5-utils/src/verifylicense.ts deleted file mode 100644 index 3b6a3dfd0fd..00000000000 --- a/packages/ckeditor5-utils/src/verifylicense.ts +++ /dev/null @@ -1,108 +0,0 @@ -/** - * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. - * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license - */ - -/** - * @module utils/verifylicense - */ - -import { releaseDate } from './version.js'; - -/** - * Possible states of the key after verification. - */ -export type VerifiedKeyStatus = 'VALID' | 'INVALID'; - -/** - * Checks whether the given string contains information that allows you to verify the license status. - * - * @param token The string to check. - * @returns String that represents the state of given `token` parameter. - */ -export default function verifyLicense( token: string | undefined ): VerifiedKeyStatus { - // This function implements naive and partial license key check mechanism, - // used only to decide whether to show or hide the "Powered by CKEditor" logo. - // - // You can read the reasoning behind showing the logo to unlicensed (GPL) users - // in this thread: https://github.com/ckeditor/ckeditor5/issues/14082. - // - // We firmly believe in the values behind creating open-source software, even when that - // means keeping the license verification logic open for everyone to see. - // - // Please keep this code intact. Thank you for your understanding. - - function oldTokenCheck( token: string ): VerifiedKeyStatus { - if ( token.length >= 40 && token.length <= 255 ) { - return 'VALID'; - } else { - return 'INVALID'; - } - } - - // TODO: issue ci#3175 - - if ( !token ) { - return 'INVALID'; - } - - let decryptedData = ''; - - try { - decryptedData = atob( token ); - } catch ( e ) { - return 'INVALID'; - } - - const splittedDecryptedData = decryptedData.split( '-' ); - - const firstElement = splittedDecryptedData[ 0 ]; - const secondElement = splittedDecryptedData[ 1 ]; - - if ( !secondElement ) { - return oldTokenCheck( token ); - } - - try { - atob( secondElement ); - } catch ( e ) { - try { - atob( firstElement ); - - if ( !atob( firstElement ).length ) { - return oldTokenCheck( token ); - } - } catch ( e ) { - return oldTokenCheck( token ); - } - } - - if ( firstElement.length < 40 || firstElement.length > 255 ) { - return 'INVALID'; - } - - let decryptedSecondElement = ''; - - try { - atob( firstElement ); - decryptedSecondElement = atob( secondElement ); - } catch ( e ) { - return 'INVALID'; - } - - if ( decryptedSecondElement.length !== 8 ) { - return 'INVALID'; - } - - const year = Number( decryptedSecondElement.substring( 0, 4 ) ); - const monthIndex = Number( decryptedSecondElement.substring( 4, 6 ) ) - 1; - const day = Number( decryptedSecondElement.substring( 6, 8 ) ); - - const date = new Date( year, monthIndex, day ); - - if ( date < releaseDate || isNaN( Number( date ) ) ) { - return 'INVALID'; - } - - return 'VALID'; -} diff --git a/packages/ckeditor5-utils/tests/verifylicense.js b/packages/ckeditor5-utils/tests/verifylicense.js deleted file mode 100644 index f59fa30e394..00000000000 --- a/packages/ckeditor5-utils/tests/verifylicense.js +++ /dev/null @@ -1,114 +0,0 @@ -/** - * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. - * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license - */ - -import verifyLicense from '../src/verifylicense.js'; - -describe( 'utils', () => { - describe( 'verify', () => { - describe( 'should return `VALID`', () => { - it( 'when date is later than the release date', () => { - // new, future - // eslint-disable-next-line max-len - const string = 'dG9vZWFzZXRtcHNsaXVyb3JsbWlkbXRvb2Vhc2V0bXBzbGl1cm9ybG1pZG10b29lYXNldG1wc2xpdXJvcmxtaWRtLU1qQTBOREEyTVRJPQ=='; - - expect( verifyLicense( string ) ).to.be.equal( 'VALID' ); - } ); - - it( 'when old token format is given', () => { - // old - // eslint-disable-next-line max-len - const string = 'YWZvb2JkcnphYXJhZWJvb290em9wbWJvbHJ1c21sZnJlYmFzdG1paXN1cm1tZmllenJhb2F0YmFvcmxvb3B6aWJvYWRiZWZzYXJ0bW9ibG8='; - - expect( verifyLicense( string ) ).to.be.equal( 'VALID' ); - } ); - - it( 'when old token format is given with a special sign', () => { - const string = 'LWRsZ2h2bWxvdWhnbXZsa3ZkaGdzZGhtdmxrc2htZ3Nma2xnaGxtcDk4N212Z3V3OTU4NHc5bWdtdw=='; - - expect( verifyLicense( string ) ).to.be.equal( 'VALID' ); - } ); - - it( 'when old token is splitted', () => { - // eslint-disable-next-line max-len - const string = 'ZXNybGl1aG1jbGlldWdtbHdpZWgvIUAjNW1nbGNlXVtcd2l1Z2NsZWpnbWNsc2lkZmdjbHNpZGZoZ2xjc2Rnc25jZGZnaGNubHMtd3A5bWN5dDlwaGdtcGM5d2g4dGc3Y3doODdvaGddW10hQCMhdG5jN293NTg0aGdjbzhud2U4Z2Nodw=='; - - expect( verifyLicense( string ) ).to.be.equal( 'VALID' ); - } ); - } ); - - describe( 'should return `INVALID`', () => { - it( 'when token is empty', () => { - expect( verifyLicense( '' ) ).to.be.equal( 'INVALID' ); - } ); - - it( 'when token is not passed', () => { - expect( verifyLicense( ) ).to.be.equal( 'INVALID' ); - } ); - - describe( 'new', () => { - it( 'first too short', () => { - expect( verifyLicense( 'Wm05dlltRnktTWpBeU5UQXhNREU9' ) ).to.be.equal( 'INVALID' ); - } ); - - it( 'first too long', () => { - // eslint-disable-next-line max-len - const string = 'YzNSbGJTQmxjbkp2Y2pvZ2JtVjBPanBGVWxKZlFreFBRMHRGUkY5Q1dWOURURWxGVGxSemRHVnRJR1Z5Y205eU9pQnVaWFE2T2tWU1VsOUNURTlEUzBWRVgwSlpYME5NU1VWT1ZITjBaVzBnWlhKeWIzSTZJRzVsZERvNlJWSlNYMEpNVDBOTFJVUmZRbGxmUTB4SlJVNVVjM1JsYlNCbGNuSnZjam9nYm1WME9qcEZVbEpmUWt4UFEwdEZSRjlDV1Y5RFRFbEZUbFJ6ZEdWdElHVnljbTl5T2lCdVpYUTZPa1ZTVWw5Q1RFOURTMFZFWDBKWlgwTk1TVVZPVkhOMFpXMGdaWEp5YjNJNklHNWxkRG82UlZKU1gwSk1UME5MUlVSZlFsbGZRMHhKUlU1VS1NakF5TlRBeE1ERT0='; - - expect( verifyLicense( string ) ).to.be.equal( 'INVALID' ); - } ); - - it( 'first wrong format', () => { - const string = 'ZGx1Z2hjbXNsaXVkZ2NobXN8IjolRVdFVnwifCJEVnxERyJXJSUkXkVSVHxWIll8UkRUIkJTfFIlQiItTWpBeU16RXlNekU9'; - - expect( verifyLicense( string ) ).to.be.equal( 'INVALID' ); - } ); - - it( 'when date is invalid', () => { - // invalid = shorten than expected - - const string = 'enN6YXJ0YWxhYWZsaWViYnRvcnVpb3Jvb3BzYmVkYW9tcm1iZm9vbS1NVGs1TnpFeA=='; - - expect( verifyLicense( string ) ).to.be.equal( 'INVALID' ); - } ); - - it( 'wrong second part format', () => { - const string = 'Ylc5MGIyeDBiMkZ5YzJsbGMybHNjR1Z0Y20xa2RXMXZkRzlzZEc5aGNuTnBaWE09LVptOXZZbUZ5WW1FPQ=='; - - expect( verifyLicense( string ) ).to.be.equal( 'INVALID' ); - } ); - - it( 'when wrong string passed', () => { - // # instead of second part - const string = 'Ylc5MGIyeDBiMkZ5YzJsbGMybHNjR1Z0Y20xa2RXMXZkRzlzZEc5aGNuTnBaWE09LSM='; - - expect( verifyLicense( string ) ).to.be.equal( 'INVALID' ); - } ); - - it( 'when date is earlier than the release date', () => { - // new, past - // eslint-disable-next-line max-len - const string = 'Y0dWc1lYVmxhWE4wYlc5a2MyMXBiRzEwY205eWIzQmxiR0YxWldsemRHMXZaSE50YVd4dGRISnZjbTlrYzJGa2MyRmtjMkU9LU1UazRNREF4TURFPQ=='; - - expect( verifyLicense( string ) ).to.be.equal( 'INVALID' ); - } ); - } ); - - describe( 'old', () => { - it( 'when date is missing', () => { - const string = 'dG1wb3Rhc2llc3VlbHJtZHJvbWlsbw=='; - - expect( verifyLicense( string ) ).to.be.equal( 'INVALID' ); - } ); - } ); - - it( 'when passed variable is invalid', () => { - // invalid - const string = 'foobarbaz'; - - expect( verifyLicense( string ) ).to.be.equal( 'INVALID' ); - } ); - } ); - } ); -} ); From 526e4f78f22b87edb3d2f4a8dd17ec0a89456c28 Mon Sep 17 00:00:00 2001 From: Marta Motyczynska Date: Tue, 23 Apr 2024 21:30:40 +0200 Subject: [PATCH 006/256] Basic tests for usage endpoint. --- .../ckeditor5-core/tests/editor/editor.js | 97 ++++++++++++++++++- 1 file changed, 96 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5-core/tests/editor/editor.js b/packages/ckeditor5-core/tests/editor/editor.js index b9305280427..de2b21f9e31 100644 --- a/packages/ckeditor5-core/tests/editor/editor.js +++ b/packages/ckeditor5-core/tests/editor/editor.js @@ -3,7 +3,7 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ -/* globals document, window, setTimeout, console */ +/* globals document, window, setTimeout, console, Response, globalThis */ import Editor from '../../src/editor/editor.js'; import Context from '../../src/context.js'; @@ -282,6 +282,101 @@ describe( 'Editor', () => { expect( editor.isReadOnly ).to.be.true; } ); } ); + + describe( 'usage endpoint', () => { + it( 'should send request with telemetry data if license key contains a usage endpoint', () => { + const fetchStub = sinon.stub( window, 'fetch' ); + + // eslint-disable-next-line max-len + const licenseKey = 'foo.eyJleHAiOjM3ODY5MTIwMDAsImp0aSI6IjE0ZWUyZDliLTFlZDktNGEwNi05NmQwLTRmYzc5YjQxMzJiOSIsInVzYWdlRW5kcG9pbnQiOiJodHRwczovL2NrZWRpdG9yLmNvbSJ9.bar'; + const editor = new TestEditor( { licenseKey } ); + + editor.fire( 'ready' ); + + sinon.assert.calledOnce( fetchStub ); + + const sentData = JSON.parse( fetchStub.firstCall.lastArg.body ); + + expect( sentData.license ).to.equal( licenseKey ); + expect( sentData.telemetry ).to.deep.equal( { editorVersion: globalThis.CKEDITOR_VERSION } ); + } ); + + it( 'should not send any request if license key does not contain a usage endpoint', () => { + const fetchStub = sinon.stub( window, 'fetch' ); + + // eslint-disable-next-line max-len + const licenseKey = 'foo.eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjM3ODY5MTIwMDAsImp0aSI6IjljY2IzNDQzLWMzN2EtNGE4Yy1iNDg3LTBjYzNkMjlmNTVjYyJ9.DxpsxsxRSqMv75JmGxK1xLnaThmMOaDeAeqEiq08FEC7x_sSGWkvf3v-cbDD9aRWnz9vYRm1WOoXzlD2e-j27g.bar'; + const editor = new TestEditor( { licenseKey } ); + + editor.fire( 'ready' ); + + sinon.assert.notCalled( fetchStub ); + } ); + + it( 'should not throw an error if response status is not ok (HTTP 500)', () => { + const fetchStub = sinon.stub( window, 'fetch' ).resolves( new Response( null, { status: 500 } ) ); + + // eslint-disable-next-line max-len + const licenseKey = 'foo.eyJleHAiOjM3ODY5MTIwMDAsImp0aSI6IjE0ZWUyZDliLTFlZDktNGEwNi05NmQwLTRmYzc5YjQxMzJiOSIsInVzYWdlRW5kcG9pbnQiOiJodHRwczovL2NrZWRpdG9yLmNvbSJ9.bar'; + const editor = new TestEditor( { licenseKey } ); + + expect( () => { + editor.fire( 'ready' ); + } ).to.not.throw(); + + sinon.assert.calledOnce( fetchStub ); + } ); + + it( 'should display warning and block the editor when usage status is not ok', done => { + const fetchStub = sinon.stub( window, 'fetch' ).resolves( { + ok: true, + json: () => Promise.resolve( { + status: 'foo' + } ) + } ); + const warnStub = testUtils.sinon.stub( console, 'warn' ); + + // eslint-disable-next-line max-len + const licenseKey = 'foo.eyJleHAiOjM3ODY5MTIwMDAsImp0aSI6IjE0ZWUyZDliLTFlZDktNGEwNi05NmQwLTRmYzc5YjQxMzJiOSIsInVzYWdlRW5kcG9pbnQiOiJodHRwczovL2NrZWRpdG9yLmNvbSJ9.bar'; + const editor = new TestEditor( { licenseKey } ); + + editor.fire( 'ready' ); + + setTimeout( () => { + sinon.assert.calledOnce( fetchStub ); + sinon.assert.calledOnce( warnStub ); + sinon.assert.calledWithMatch( warnStub, 'The licensed usage count exceeded' ); + expect( editor.isReadOnly ).to.be.true; + done(); + }, 1 ); + } ); + + it( 'should display additional warning when usage status is not ok and message is provided', done => { + const fetchStub = sinon.stub( window, 'fetch' ).resolves( { + ok: true, + json: () => Promise.resolve( { + status: 'foo', + message: 'bar' + } ) + } ); + const warnStub = testUtils.sinon.stub( console, 'warn' ); + + // eslint-disable-next-line max-len + const licenseKey = 'foo.eyJleHAiOjM3ODY5MTIwMDAsImp0aSI6IjE0ZWUyZDliLTFlZDktNGEwNi05NmQwLTRmYzc5YjQxMzJiOSIsInVzYWdlRW5kcG9pbnQiOiJodHRwczovL2NrZWRpdG9yLmNvbSJ9.bar'; + const editor = new TestEditor( { licenseKey } ); + + editor.fire( 'ready' ); + + setTimeout( () => { + sinon.assert.calledOnce( fetchStub ); + sinon.assert.calledTwice( warnStub ); + sinon.assert.calledWithMatch( warnStub.getCall( 0 ), 'bar' ); + sinon.assert.calledWithMatch( warnStub.getCall( 1 ), 'The licensed usage count exceeded' ); + expect( editor.isReadOnly ).to.be.true; + done(); + }, 1 ); + } ); + } ); } ); describe( 'context integration', () => { From 67bf03efa14e0b7232104610ceadaad40a759f4b Mon Sep 17 00:00:00 2001 From: Marta Motyczynska Date: Wed, 24 Apr 2024 13:29:22 +0200 Subject: [PATCH 007/256] Update tests for usage endpoint. --- .../ckeditor5-core/tests/editor/editor.js | 55 +++++++++++-------- 1 file changed, 33 insertions(+), 22 deletions(-) diff --git a/packages/ckeditor5-core/tests/editor/editor.js b/packages/ckeditor5-core/tests/editor/editor.js index de2b21f9e31..efd8754a4ef 100644 --- a/packages/ckeditor5-core/tests/editor/editor.js +++ b/packages/ckeditor5-core/tests/editor/editor.js @@ -305,7 +305,7 @@ describe( 'Editor', () => { const fetchStub = sinon.stub( window, 'fetch' ); // eslint-disable-next-line max-len - const licenseKey = 'foo.eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjM3ODY5MTIwMDAsImp0aSI6IjljY2IzNDQzLWMzN2EtNGE4Yy1iNDg3LTBjYzNkMjlmNTVjYyJ9.DxpsxsxRSqMv75JmGxK1xLnaThmMOaDeAeqEiq08FEC7x_sSGWkvf3v-cbDD9aRWnz9vYRm1WOoXzlD2e-j27g.bar'; + const licenseKey = 'foo.eyJleHAiOjM3ODY5MTIwMDAsImp0aSI6IjM0YzVkZjUwLTA4NmQtNGYyOC1iMGRlLWE2ZmQxNmNjOGU0MSJ9.bar'; const editor = new TestEditor( { licenseKey } ); editor.fire( 'ready' ); @@ -313,21 +313,30 @@ describe( 'Editor', () => { sinon.assert.notCalled( fetchStub ); } ); - it( 'should not throw an error if response status is not ok (HTTP 500)', () => { + it( 'should display error on the console and not block the editor if response status is not ok (HTTP 500)', async () => { const fetchStub = sinon.stub( window, 'fetch' ).resolves( new Response( null, { status: 500 } ) ); + const originalRejectionHandler = window.onunhandledrejection; + let capturedError = null; + + window.onunhandledrejection = evt => { + capturedError = evt.reason.message; + return true; + }; // eslint-disable-next-line max-len const licenseKey = 'foo.eyJleHAiOjM3ODY5MTIwMDAsImp0aSI6IjE0ZWUyZDliLTFlZDktNGEwNi05NmQwLTRmYzc5YjQxMzJiOSIsInVzYWdlRW5kcG9pbnQiOiJodHRwczovL2NrZWRpdG9yLmNvbSJ9.bar'; const editor = new TestEditor( { licenseKey } ); - expect( () => { - editor.fire( 'ready' ); - } ).to.not.throw(); + editor.fire( 'ready' ); + await wait( 1 ); + window.onunhandledrejection = originalRejectionHandler; sinon.assert.calledOnce( fetchStub ); + expect( capturedError ).to.equal( 'HTTP Response: 500' ); + expect( editor.isReadOnly ).to.be.false; } ); - it( 'should display warning and block the editor when usage status is not ok', done => { + it( 'should display warning and block the editor when usage status is not ok', async () => { const fetchStub = sinon.stub( window, 'fetch' ).resolves( { ok: true, json: () => Promise.resolve( { @@ -341,17 +350,15 @@ describe( 'Editor', () => { const editor = new TestEditor( { licenseKey } ); editor.fire( 'ready' ); + await wait( 1 ); - setTimeout( () => { - sinon.assert.calledOnce( fetchStub ); - sinon.assert.calledOnce( warnStub ); - sinon.assert.calledWithMatch( warnStub, 'The licensed usage count exceeded' ); - expect( editor.isReadOnly ).to.be.true; - done(); - }, 1 ); + sinon.assert.calledOnce( fetchStub ); + sinon.assert.calledOnce( warnStub ); + sinon.assert.calledWithMatch( warnStub, 'The licensed usage count exceeded' ); + expect( editor.isReadOnly ).to.be.true; } ); - it( 'should display additional warning when usage status is not ok and message is provided', done => { + it( 'should display additional warning when usage status is not ok and message is provided', async () => { const fetchStub = sinon.stub( window, 'fetch' ).resolves( { ok: true, json: () => Promise.resolve( { @@ -366,15 +373,13 @@ describe( 'Editor', () => { const editor = new TestEditor( { licenseKey } ); editor.fire( 'ready' ); + await wait( 1 ); - setTimeout( () => { - sinon.assert.calledOnce( fetchStub ); - sinon.assert.calledTwice( warnStub ); - sinon.assert.calledWithMatch( warnStub.getCall( 0 ), 'bar' ); - sinon.assert.calledWithMatch( warnStub.getCall( 1 ), 'The licensed usage count exceeded' ); - expect( editor.isReadOnly ).to.be.true; - done(); - }, 1 ); + sinon.assert.calledOnce( fetchStub ); + sinon.assert.calledTwice( warnStub ); + sinon.assert.calledWithMatch( warnStub.getCall( 0 ), 'bar' ); + sinon.assert.calledWithMatch( warnStub.getCall( 1 ), 'The licensed usage count exceeded' ); + expect( editor.isReadOnly ).to.be.true; } ); } ); } ); @@ -1515,3 +1520,9 @@ function getPlugins( editor ) { return Array.from( editor.plugins ) .map( entry => entry[ 1 ] ); // Get instances. } + +function wait( time ) { + return new Promise( res => { + window.setTimeout( res, time ); + } ); +} From 52026470e14128488075460cf54fc6302256d618 Mon Sep 17 00:00:00 2001 From: Marta Motyczynska Date: Wed, 24 Apr 2024 14:32:35 +0200 Subject: [PATCH 008/256] Remove unnecessary comments. --- packages/ckeditor5-ui/tests/editorui/poweredby.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/ckeditor5-ui/tests/editorui/poweredby.js b/packages/ckeditor5-ui/tests/editorui/poweredby.js index 35ea23bcf39..8c41978083b 100644 --- a/packages/ckeditor5-ui/tests/editorui/poweredby.js +++ b/packages/ckeditor5-ui/tests/editorui/poweredby.js @@ -118,9 +118,6 @@ describe( 'PoweredBy', () => { it( 'should create the balloon when a non-white-label license key is configured', async () => { const editor = await createEditor( element, { - // eslint-disable-next-line max-len - // https://github.com/ckeditor/ckeditor5/blob/226bf243d1eb8bae2d447f631d6f5d9961bc6541/packages/ckeditor5-utils/tests/verifylicense.js#L14 - // eslint-disable-next-line max-len licenseKey: 'foo.eyJhYmMiOjF9.bar' } ); From 1a3ff691058355b10da4368e19e9f36672517311 Mon Sep 17 00:00:00 2001 From: Marta Motyczynska Date: Wed, 24 Apr 2024 15:21:51 +0200 Subject: [PATCH 009/256] Update license keys to get rid of warnings. --- packages/ckeditor5-ui/tests/editorui/poweredby.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ckeditor5-ui/tests/editorui/poweredby.js b/packages/ckeditor5-ui/tests/editorui/poweredby.js index 8c41978083b..56d5a60d5c4 100644 --- a/packages/ckeditor5-ui/tests/editorui/poweredby.js +++ b/packages/ckeditor5-ui/tests/editorui/poweredby.js @@ -85,7 +85,7 @@ describe( 'PoweredBy', () => { it( 'should not create the balloon when a white-label license key is configured', async () => { const editor = await createEditor( element, { - licenseKey: 'foo.eyJ3aGl0ZUxhYmVsIjp0cnVlfQ.bar' + licenseKey: 'foo.eyJ3aGl0ZUxhYmVsIjp0cnVlLCJleHAiOjIyMDg5ODg4MDB9.bar' } ); expect( editor.ui.poweredBy._balloonView ).to.be.null; @@ -99,7 +99,7 @@ describe( 'PoweredBy', () => { it( 'should create the balloon when a white-label license key is configured and `forceVisible` is set to true', async () => { const editor = await createEditor( element, { - licenseKey: 'foo.eyJ3aGl0ZUxhYmVsIjp0cnVlfQ.bar', + licenseKey: 'foo.eyJ3aGl0ZUxhYmVsIjp0cnVlLCJleHAiOjIyMDg5ODg4MDB9.bar', ui: { poweredBy: { forceVisible: true @@ -118,7 +118,7 @@ describe( 'PoweredBy', () => { it( 'should create the balloon when a non-white-label license key is configured', async () => { const editor = await createEditor( element, { - licenseKey: 'foo.eyJhYmMiOjF9.bar' + licenseKey: 'foo.eyJhYmMiOjEsImV4cCI6MjIwODk4ODgwMH0.bar' } ); expect( editor.ui.poweredBy._balloonView ).to.be.null; From c6f71835db96e327a7a56d5a19b24e07f8f614f4 Mon Sep 17 00:00:00 2001 From: Marta Motyczynska Date: Fri, 26 Apr 2024 16:50:43 +0200 Subject: [PATCH 010/256] Add hash-based verification of license. --- packages/ckeditor5-core/src/editor/editor.ts | 41 +++++++- .../ckeditor5-core/tests/editor/editor.js | 72 +++++++++----- packages/ckeditor5-utils/src/crc32.ts | 81 +++++++++++++++ packages/ckeditor5-utils/src/index.ts | 1 + packages/ckeditor5-utils/tests/crc32.js | 99 +++++++++++++++++++ 5 files changed, 268 insertions(+), 26 deletions(-) create mode 100644 packages/ckeditor5-utils/src/crc32.ts create mode 100644 packages/ckeditor5-utils/tests/crc32.js diff --git a/packages/ckeditor5-core/src/editor/editor.ts b/packages/ckeditor5-core/src/editor/editor.ts index 8ecd5dfb2aa..b876fa6f2a2 100644 --- a/packages/ckeditor5-core/src/editor/editor.ts +++ b/packages/ckeditor5-core/src/editor/editor.ts @@ -14,9 +14,11 @@ import { parseBase64EncodedObject, releaseDate, uid, + crc32, type Locale, type LocaleTranslate, - type ObservableChangeEvent + type ObservableChangeEvent, + type CRCData } from '@ckeditor/ckeditor5-utils'; import { @@ -682,7 +684,7 @@ export default abstract class Editor extends ObservableMixin() { return; } - if ( !licensePayload.exp ) { + if ( !hasAllRequiredFields( licensePayload ) ) { blockEditor( this, 'licenseFormatInvalid', 'The format of the license key is invalid.' ); return; @@ -692,6 +694,14 @@ export default abstract class Editor extends ObservableMixin() { if ( expirationDate < releaseDate ) { blockEditor( this, 'licenseExpired', 'The validation period for the editor license key has expired.' ); + + return; + } + + if ( crc32( getCrcInputData( licensePayload ), true ) != licensePayload.verificationCode ) { + blockEditor( this, 'licenseFormatInvalid', 'The format of the license key is invalid.' ); + + return; } if ( licensePayload.usageEndpoint ) { @@ -728,6 +738,33 @@ export default abstract class Editor extends ObservableMixin() { editor.enableReadOnlyMode( reason ); } + + function hasAllRequiredFields( licensePayload: Record ) { + const requiredFields = [ 'exp', 'jti', 'verificationCode' ]; + + return requiredFields.every( field => field in licensePayload ); + } + + /** + * Returns an array of values that are used to calculate the CRC32 checksum. + */ + function getCrcInputData( licensePayload: Record ): CRCData { + const keysToCheck = [ + 'exp', + 'licensedHosts', + 'usageEndpoint', + 'distributionChannel', + 'whiteLabel', + 'licenseType', + 'features' + ]; + + const filteredValues = keysToCheck + .filter( key => licensePayload[ key ] !== undefined && licensePayload[ key ] !== null ) + .map( key => licensePayload[ key ] ); + + return [ ...filteredValues ] as CRCData; + } } private async _sendUsageRequest( diff --git a/packages/ckeditor5-core/tests/editor/editor.js b/packages/ckeditor5-core/tests/editor/editor.js index efd8754a4ef..2b467c6f236 100644 --- a/packages/ckeditor5-core/tests/editor/editor.js +++ b/packages/ckeditor5-core/tests/editor/editor.js @@ -223,19 +223,46 @@ describe( 'Editor', () => { stub = testUtils.sinon.stub( console, 'warn' ); } ); - it( 'should not block the editor when the license key is valid (expiration date in the future)', () => { - // eslint-disable-next-line max-len - const licenseKey = 'foo.eyJleHAiOjIyMDg5ODg4MDAsImp0aSI6ImZjYjE0ZDAwLTJmZmItNDQxMy1iMzM3LTljMjhiOTE0MjRjMCIsImxpY2Vuc2VUeXBlIjoidHJpYWwifQ.bar'; + describe( 'rquired fields in the license key', () => { + it( 'should not block the editor when required fields are provided and are valid', () => { + const licenseKey = 'foo.eyJleHAiOjIyMDg5ODg4MDAsImp0aSI6ImZvbyIsInZlcmlmaWNhdGlvbkNvZGUiOiJjNTU2YWQ3NCJ9.bar'; - const editor = new TestEditor( { licenseKey } ); + const editor = new TestEditor( { licenseKey } ); - sinon.assert.notCalled( stub ); - expect( editor.isReadOnly ).to.be.false; + sinon.assert.notCalled( stub ); + expect( editor.isReadOnly ).to.be.false; + } ); + + it( 'should block the editor when the `exp` field is missing', () => { + const licenseKey = 'foo.eyJqdGkiOiJmb28iLCJ2ZXJpZmljYXRpb25Db2RlIjoiMCJ9.bar'; + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.calledWithMatch( stub, 'The format of the license key is invalid.' ); + expect( editor.isReadOnly ).to.be.true; + } ); + + it( 'should block the editor when the `jti` field is missing', () => { + const licenseKey = 'foo.eyJleHAiOjIyMDg5ODg4MDAsInZlcmlmaWNhdGlvbkNvZGUiOiJjNTU2YWQ3NCJ9.bar'; + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.calledWithMatch( stub, 'The format of the license key is invalid.' ); + expect( editor.isReadOnly ).to.be.true; + } ); + + it( 'should block the editor when the `verificationCode` field is missing', () => { + const licenseKey = 'foo.eyJleHAiOjIyMDg5ODg4MDAsImp0aSI6ImZvbyJ9.bar'; + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.calledWithMatch( stub, 'The format of the license key is invalid.' ); + expect( editor.isReadOnly ).to.be.true; + } ); } ); it( 'should block the editor when the license key is not valid (expiration date in the past)', () => { // eslint-disable-next-line max-len - const licenseKey = 'foo.eyJleHAiOjE3MDQwNjcyMDAsImp0aSI6IjZlY2JkZjU2LTVlYjMtNGIyYy05NWI1LWU5M2MwZDZiNmZmMSIsImxpY2Vuc2VUeXBlIjoidHJpYWwifQ.bar'; + const licenseKey = 'foo.eyJleHAiOjE3MDQwNjcyMDAsImp0aSI6ImZvbyIsInZlcmlmaWNhdGlvbkNvZGUiOiI2ZTlkNDM2NSJ9.bar'; const editor = new TestEditor( { licenseKey } ); @@ -243,9 +270,8 @@ describe( 'Editor', () => { expect( editor.isReadOnly ).to.be.true; } ); - it( 'should block the editor when the license key has wrong format (missing header part)', () => { - // eslint-disable-next-line max-len - const licenseKey = 'eyJleHAiOjIyMDg5ODg4MDAsImp0aSI6ImZjYjE0ZDAwLTJmZmItNDQxMy1iMzM3LTljMjhiOTE0MjRjMCIsImxpY2Vuc2VUeXBlIjoidHJpYWwifQ.bar'; + it( 'should block the editor when the license key has wrong format (wrong verificationCode)', () => { + const licenseKey = 'foo.eyJleHAiOjIyMDg5ODg4MDAsImp0aSI6ImZvbyIsInZlcmlmaWNhdGlvbkNvZGUiOiIxMTExMTExMSJ9.bar'; const editor = new TestEditor( { licenseKey } ); @@ -253,9 +279,8 @@ describe( 'Editor', () => { expect( editor.isReadOnly ).to.be.true; } ); - it( 'should block the editor when the license key has wrong format (payload does not start with `ey`)', () => { - // eslint-disable-next-line max-len - const licenseKey = 'foo.JleHAiOjIyMDg5ODg4MDAsImp0aSI6ImZjYjE0ZDAwLTJmZmItNDQxMy1iMzM3LTljMjhiOTE0MjRjMCIsImxpY2Vuc2VUeXBlIjoidHJpYWwifQ.bar'; + it( 'should block the editor when the license key has wrong format (missing header part)', () => { + const licenseKey = 'eyJleHAiOjIyMDg5ODg4MDAsImp0aSI6ImZvbyIsInZlcmlmaWNhdGlvbkNvZGUiOiJjNTU2YWQ3NCJ9.bar'; const editor = new TestEditor( { licenseKey } ); @@ -263,9 +288,8 @@ describe( 'Editor', () => { expect( editor.isReadOnly ).to.be.true; } ); - it( 'should block the editor when the license key has wrong format (payload not parsable as a JSON object)', () => { - // eslint-disable-next-line max-len - const licenseKey = 'foo.eyZm9v.bar'; + it( 'should block the editor when the license key has wrong format (payload does not start with `ey`)', () => { + const licenseKey = 'foo.JleHAiOjIyMDg5ODg4MDAsImp0aSI6ImZvbyIsInZlcmlmaWNhdGlvbkNvZGUiOiJjNTU2YWQ3NCJ9.bar'; const editor = new TestEditor( { licenseKey } ); @@ -273,9 +297,9 @@ describe( 'Editor', () => { expect( editor.isReadOnly ).to.be.true; } ); - it( 'should block the editor when the license key has wrong format (missing expiration date)', () => { - // eslint-disable-next-line max-len - const licenseKey = 'foo.eyJqdGkiOiI1ZjY3ODk5YS04OWQ4LTQxYWUtOWU4Yi1mMzhiMTIzZjI3YjYifQ.bar'; + it( 'should block the editor when the license key has wrong format (payload not parsable as a JSON object)', () => { + const licenseKey = 'foo.eyZm9v.bar'; + const editor = new TestEditor( { licenseKey } ); sinon.assert.calledWithMatch( stub, 'The format of the license key is invalid.' ); @@ -288,7 +312,7 @@ describe( 'Editor', () => { const fetchStub = sinon.stub( window, 'fetch' ); // eslint-disable-next-line max-len - const licenseKey = 'foo.eyJleHAiOjM3ODY5MTIwMDAsImp0aSI6IjE0ZWUyZDliLTFlZDktNGEwNi05NmQwLTRmYzc5YjQxMzJiOSIsInVzYWdlRW5kcG9pbnQiOiJodHRwczovL2NrZWRpdG9yLmNvbSJ9.bar'; + const licenseKey = 'foo.eyJleHAiOjM3ODY5MTIwMDAsImp0aSI6ImZvbyIsInVzYWdlRW5kcG9pbnQiOiJodHRwczovL2NrZWRpdG9yLmNvbSIsInZlcmlmaWNhdGlvbkNvZGUiOiJjZWE0ZjQ2MSJ9.bar'; const editor = new TestEditor( { licenseKey } ); editor.fire( 'ready' ); @@ -305,7 +329,7 @@ describe( 'Editor', () => { const fetchStub = sinon.stub( window, 'fetch' ); // eslint-disable-next-line max-len - const licenseKey = 'foo.eyJleHAiOjM3ODY5MTIwMDAsImp0aSI6IjM0YzVkZjUwLTA4NmQtNGYyOC1iMGRlLWE2ZmQxNmNjOGU0MSJ9.bar'; + const licenseKey = 'foo.eyJleHAiOjM3ODY5MTIwMDAsImp0aSI6ImZvbyIsInZlcmlmaWNhdGlvbkNvZGUiOiI4ZjY3MzA0MCJ9.bar'; const editor = new TestEditor( { licenseKey } ); editor.fire( 'ready' ); @@ -324,7 +348,7 @@ describe( 'Editor', () => { }; // eslint-disable-next-line max-len - const licenseKey = 'foo.eyJleHAiOjM3ODY5MTIwMDAsImp0aSI6IjE0ZWUyZDliLTFlZDktNGEwNi05NmQwLTRmYzc5YjQxMzJiOSIsInVzYWdlRW5kcG9pbnQiOiJodHRwczovL2NrZWRpdG9yLmNvbSJ9.bar'; + const licenseKey = 'foo.eyJleHAiOjM3ODY5MTIwMDAsImp0aSI6ImZvbyIsInVzYWdlRW5kcG9pbnQiOiJodHRwczovL2NrZWRpdG9yLmNvbSIsInZlcmlmaWNhdGlvbkNvZGUiOiJjZWE0ZjQ2MSJ9.bar'; const editor = new TestEditor( { licenseKey } ); editor.fire( 'ready' ); @@ -346,7 +370,7 @@ describe( 'Editor', () => { const warnStub = testUtils.sinon.stub( console, 'warn' ); // eslint-disable-next-line max-len - const licenseKey = 'foo.eyJleHAiOjM3ODY5MTIwMDAsImp0aSI6IjE0ZWUyZDliLTFlZDktNGEwNi05NmQwLTRmYzc5YjQxMzJiOSIsInVzYWdlRW5kcG9pbnQiOiJodHRwczovL2NrZWRpdG9yLmNvbSJ9.bar'; + const licenseKey = 'foo.eyJleHAiOjM3ODY5MTIwMDAsImp0aSI6ImZvbyIsInVzYWdlRW5kcG9pbnQiOiJodHRwczovL2NrZWRpdG9yLmNvbSIsInZlcmlmaWNhdGlvbkNvZGUiOiJjZWE0ZjQ2MSJ9.bar'; const editor = new TestEditor( { licenseKey } ); editor.fire( 'ready' ); @@ -369,7 +393,7 @@ describe( 'Editor', () => { const warnStub = testUtils.sinon.stub( console, 'warn' ); // eslint-disable-next-line max-len - const licenseKey = 'foo.eyJleHAiOjM3ODY5MTIwMDAsImp0aSI6IjE0ZWUyZDliLTFlZDktNGEwNi05NmQwLTRmYzc5YjQxMzJiOSIsInVzYWdlRW5kcG9pbnQiOiJodHRwczovL2NrZWRpdG9yLmNvbSJ9.bar'; + const licenseKey = 'foo.eyJleHAiOjM3ODY5MTIwMDAsImp0aSI6ImZvbyIsInVzYWdlRW5kcG9pbnQiOiJodHRwczovL2NrZWRpdG9yLmNvbSIsInZlcmlmaWNhdGlvbkNvZGUiOiJjZWE0ZjQ2MSJ9.bar'; const editor = new TestEditor( { licenseKey } ); editor.fire( 'ready' ); diff --git a/packages/ckeditor5-utils/src/crc32.ts b/packages/ckeditor5-utils/src/crc32.ts new file mode 100644 index 00000000000..2e4d0097d8c --- /dev/null +++ b/packages/ckeditor5-utils/src/crc32.ts @@ -0,0 +1,81 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/** + * @module utils/crc32 + */ + +/** + * Generates a CRC lookup table. + * This function creates and returns a 256-element array of pre-computed CRC values for quick CRC calculation. + * It uses the polynomial 0xEDB88320 to compute each value in the loop, optimizing future CRC calculations. + */ +function makeCrcTable(): Array { + const crcTable: Array = []; + + for ( let n = 0; n < 256; n++ ) { + let c: number = n; + + for ( let k = 0; k < 8; k++ ) { + if ( c & 1 ) { + c = 0xEDB88320 ^ ( c >>> 1 ); + } else { + c = c >>> 1; + } + } + + crcTable[ n ] = c; + } + + return crcTable; +} + +/** + * Calculates CRC-32 checksum for a given inputData to verify the integrity of data. + * + * @param inputData Accepts a single value (string, number, boolean), an array of strings, or an array of all of the above types. + * Non-string values are converted to strings before calculating the checksum. + * The checksum calculation is based on the concatenated string representation of the input values: + * * `crc32('foo')` is equivalent to `crc32(['foo'])` + * * `crc32(123)` is equivalent to `crc32(['123'])` + * * `crc32(true)` is equivalent to `crc32(['true'])` + * * `crc32(['foo', 123, true])` produces the same result as `crc32('foo123true')` + * * Nested arrays of strings are flattened, so `crc32([['foo', 'bar'], 'baz'])` is equivalent to `crc32(['foobar', 'baz'])` + * + * @param returnHex Optional. If set to true, the checksum is returned as a hexadecimal string. Otherwise, it is returned as a number. + * @returns The CRC-32 checksum, either as a number or a hexadecimal string if returnHex is true. + */ +export default function crc32( inputData: CRCData, returnHex: boolean = false ): number | string { + const dataArray = Array.isArray( inputData ) ? inputData : [ inputData ]; + const crcTable: Array = makeCrcTable(); + let crc: number = 0 ^ ( -1 ); + + // Convert data to a single string + const dataString: string = dataArray.map( item => { + if ( Array.isArray( item ) ) { + return item.join( '' ); + } + + return String( item ); + } ).join( '' ); + + // Calculate the CRC for the resulting string + for ( let i = 0; i < dataString.length; i++ ) { + const byte: number = dataString.charCodeAt( i ); + crc = ( crc >>> 8 ) ^ crcTable[ ( crc ^ byte ) & 0xFF ]; + } + + crc = ( crc ^ ( -1 ) ) >>> 0; // Force unsigned integer + + return returnHex ? crc.toString( 16 ) : crc; +} + +/** + * The input data for the CRC-32 checksum calculation. + * Can be a single value (string, number, boolean), an array of strings, or an array of all of the above types. + */ +export type CRCData = CRCValue | Array; + +type CRCValue = string | number | boolean | Array; diff --git a/packages/ckeditor5-utils/src/index.ts b/packages/ckeditor5-utils/src/index.ts index 391c7df49a5..4823886bfc8 100644 --- a/packages/ckeditor5-utils/src/index.ts +++ b/packages/ckeditor5-utils/src/index.ts @@ -93,6 +93,7 @@ export { default as uid } from './uid.js'; export { default as delay, type DelayedFunc } from './delay.js'; export { default as wait } from './wait.js'; export { default as parseBase64EncodedObject } from './parsebase64encodedobject.js'; +export { default as crc32, type CRCData } from './crc32.js'; export * from './unicode.js'; diff --git a/packages/ckeditor5-utils/tests/crc32.js b/packages/ckeditor5-utils/tests/crc32.js new file mode 100644 index 00000000000..ce92943e4eb --- /dev/null +++ b/packages/ckeditor5-utils/tests/crc32.js @@ -0,0 +1,99 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +import crc32 from '../src/crc32.js'; + +describe( 'crc32', () => { + describe( 'input is a single value (not an array)', () => { + it( 'should correctly calculate the CRC32 checksum for a string', () => { + const input = 'foo'; + const expectedHex = '8c736521'; + expect( crc32( input, true ) ).to.equal( expectedHex ); + } ); + + it( 'should correctly calculate the CRC32 checksum for a number', () => { + const input = 123; + const expectedHex = '884863d2'; + expect( crc32( input, true ) ).to.equal( expectedHex ); + } ); + + it( 'should correctly calculate the CRC32 checksum for a boolean', () => { + const input = true; + const expectedHex = 'fdfc4c8d'; + expect( crc32( input, true ) ).to.equal( expectedHex ); + } ); + + it( 'should correctly calculate the CRC32 checksum for an empty string', () => { + const input = ''; + const expectedHex = '0'; + expect( crc32( input, true ) ).to.equal( expectedHex ); + } ); + } ); + + describe( 'input is an array', () => { + it( 'should correctly calculate the CRC32 checksum for a string', () => { + const input = [ 'foo' ]; + const expectedHex = '8c736521'; + expect( crc32( input, true ) ).to.equal( expectedHex ); + } ); + + it( 'should correctly calculate the CRC32 checksum for a number', () => { + const input = [ 123 ]; + const expectedHex = '884863d2'; + expect( crc32( input, true ) ).to.equal( expectedHex ); + } ); + + it( 'should correctly calculate the CRC32 checksum for a boolean', () => { + const input = [ true ]; + const expectedHex = 'fdfc4c8d'; + expect( crc32( input, true ) ).to.equal( expectedHex ); + } ); + + it( 'should correctly calculate the CRC32 checksum for a table of strings', () => { + const input = [ 'foo', 'bar', 'baz' ]; + const expectedHex = '1a7827aa'; + expect( crc32( input, true ) ).to.equal( expectedHex ); + } ); + + it( 'should handle mixed data types and compute a valid CRC32 checksum', () => { + const input = [ 'foo', 123, false, [ 'bar', 'baz' ] ]; + const expectedHex = 'ee1795af'; + expect( crc32( input, true ) ).to.equal( expectedHex ); + } ); + + it( 'should correctly handle an empty array', () => { + const input = []; + const expectedHex = '0'; + expect( crc32( input, true ) ).to.equal( expectedHex ); + } ); + + it( 'should correctly handle arrays containing empty strings', () => { + const input = [ '', '', '' ]; + const expectedHex = '0'; + expect( crc32( input, true ) ).to.equal( expectedHex ); + } ); + } ); + + describe( 'return values', () => { + it( 'should return a number when returnHex is set to false', () => { + const input = [ 'foo' ]; + const result = 2356372769; + expect( crc32( input, false ) ).to.equal( result ); + } ); + + it( 'should return a number when returnHex is not set', () => { + const input = [ 'foo' ]; + const result = 2356372769; + expect( crc32( input ) ).to.equal( result ); + } ); + + it( 'should return consistent results for the same input', () => { + const input = [ 'foo', 'bar' ]; + const firstRun = crc32( input, true ); + const secondRun = crc32( input, true ); + expect( firstRun ).to.equal( secondRun ); + } ); + } ); +} ); From c57f5f5e75be54a6b5deb8cef454f63abba0147c Mon Sep 17 00:00:00 2001 From: Marta Motyczynska Date: Fri, 26 Apr 2024 17:13:07 +0200 Subject: [PATCH 011/256] Change default crc32 returnHex value to true. --- packages/ckeditor5-core/src/editor/editor.ts | 2 +- packages/ckeditor5-utils/src/crc32.ts | 7 +++-- packages/ckeditor5-utils/tests/crc32.js | 32 ++++++++++++-------- 3 files changed, 24 insertions(+), 17 deletions(-) diff --git a/packages/ckeditor5-core/src/editor/editor.ts b/packages/ckeditor5-core/src/editor/editor.ts index b876fa6f2a2..8c1557755af 100644 --- a/packages/ckeditor5-core/src/editor/editor.ts +++ b/packages/ckeditor5-core/src/editor/editor.ts @@ -698,7 +698,7 @@ export default abstract class Editor extends ObservableMixin() { return; } - if ( crc32( getCrcInputData( licensePayload ), true ) != licensePayload.verificationCode ) { + if ( crc32( getCrcInputData( licensePayload ) ) != licensePayload.verificationCode ) { blockEditor( this, 'licenseFormatInvalid', 'The format of the license key is invalid.' ); return; diff --git a/packages/ckeditor5-utils/src/crc32.ts b/packages/ckeditor5-utils/src/crc32.ts index 2e4d0097d8c..f4f114321de 100644 --- a/packages/ckeditor5-utils/src/crc32.ts +++ b/packages/ckeditor5-utils/src/crc32.ts @@ -44,10 +44,11 @@ function makeCrcTable(): Array { * * `crc32(['foo', 123, true])` produces the same result as `crc32('foo123true')` * * Nested arrays of strings are flattened, so `crc32([['foo', 'bar'], 'baz'])` is equivalent to `crc32(['foobar', 'baz'])` * - * @param returnHex Optional. If set to true, the checksum is returned as a hexadecimal string. Otherwise, it is returned as a number. - * @returns The CRC-32 checksum, either as a number or a hexadecimal string if returnHex is true. + * @param returnHex Optional. Specifies the format of the return value. If set to true, the checksum is returned as a hexadecimal string. + * If false, the checksum is returned as a numeric value. Default is true. + * @returns The CRC-32 checksum, returned as a hexadecimal string by default, or as a number if returnHex is set to false. */ -export default function crc32( inputData: CRCData, returnHex: boolean = false ): number | string { +export default function crc32( inputData: CRCData, returnHex: boolean = true ): number | string { const dataArray = Array.isArray( inputData ) ? inputData : [ inputData ]; const crcTable: Array = makeCrcTable(); let crc: number = 0 ^ ( -1 ); diff --git a/packages/ckeditor5-utils/tests/crc32.js b/packages/ckeditor5-utils/tests/crc32.js index ce92943e4eb..03fa82a5a35 100644 --- a/packages/ckeditor5-utils/tests/crc32.js +++ b/packages/ckeditor5-utils/tests/crc32.js @@ -10,25 +10,25 @@ describe( 'crc32', () => { it( 'should correctly calculate the CRC32 checksum for a string', () => { const input = 'foo'; const expectedHex = '8c736521'; - expect( crc32( input, true ) ).to.equal( expectedHex ); + expect( crc32( input ) ).to.equal( expectedHex ); } ); it( 'should correctly calculate the CRC32 checksum for a number', () => { const input = 123; const expectedHex = '884863d2'; - expect( crc32( input, true ) ).to.equal( expectedHex ); + expect( crc32( input ) ).to.equal( expectedHex ); } ); it( 'should correctly calculate the CRC32 checksum for a boolean', () => { const input = true; const expectedHex = 'fdfc4c8d'; - expect( crc32( input, true ) ).to.equal( expectedHex ); + expect( crc32( input ) ).to.equal( expectedHex ); } ); it( 'should correctly calculate the CRC32 checksum for an empty string', () => { const input = ''; const expectedHex = '0'; - expect( crc32( input, true ) ).to.equal( expectedHex ); + expect( crc32( input ) ).to.equal( expectedHex ); } ); } ); @@ -36,43 +36,43 @@ describe( 'crc32', () => { it( 'should correctly calculate the CRC32 checksum for a string', () => { const input = [ 'foo' ]; const expectedHex = '8c736521'; - expect( crc32( input, true ) ).to.equal( expectedHex ); + expect( crc32( input ) ).to.equal( expectedHex ); } ); it( 'should correctly calculate the CRC32 checksum for a number', () => { const input = [ 123 ]; const expectedHex = '884863d2'; - expect( crc32( input, true ) ).to.equal( expectedHex ); + expect( crc32( input ) ).to.equal( expectedHex ); } ); it( 'should correctly calculate the CRC32 checksum for a boolean', () => { const input = [ true ]; const expectedHex = 'fdfc4c8d'; - expect( crc32( input, true ) ).to.equal( expectedHex ); + expect( crc32( input ) ).to.equal( expectedHex ); } ); it( 'should correctly calculate the CRC32 checksum for a table of strings', () => { const input = [ 'foo', 'bar', 'baz' ]; const expectedHex = '1a7827aa'; - expect( crc32( input, true ) ).to.equal( expectedHex ); + expect( crc32( input ) ).to.equal( expectedHex ); } ); it( 'should handle mixed data types and compute a valid CRC32 checksum', () => { const input = [ 'foo', 123, false, [ 'bar', 'baz' ] ]; const expectedHex = 'ee1795af'; - expect( crc32( input, true ) ).to.equal( expectedHex ); + expect( crc32( input ) ).to.equal( expectedHex ); } ); it( 'should correctly handle an empty array', () => { const input = []; const expectedHex = '0'; - expect( crc32( input, true ) ).to.equal( expectedHex ); + expect( crc32( input ) ).to.equal( expectedHex ); } ); it( 'should correctly handle arrays containing empty strings', () => { const input = [ '', '', '' ]; const expectedHex = '0'; - expect( crc32( input, true ) ).to.equal( expectedHex ); + expect( crc32( input ) ).to.equal( expectedHex ); } ); } ); @@ -83,9 +83,15 @@ describe( 'crc32', () => { expect( crc32( input, false ) ).to.equal( result ); } ); - it( 'should return a number when returnHex is not set', () => { + it( 'should return a hexadecimal string when returnHex is true', () => { const input = [ 'foo' ]; - const result = 2356372769; + const result = '8c736521'; + expect( crc32( input ) ).to.equal( result ); + } ); + + it( 'should return a hexadecimal string when returnHex is not set', () => { + const input = [ 'foo' ]; + const result = '8c736521'; expect( crc32( input ) ).to.equal( result ); } ); From 5370861b76062d6a1f686c40cb5c2ea74a883ff3 Mon Sep 17 00:00:00 2001 From: Arkadiusz Filipczak Date: Mon, 6 May 2024 13:29:48 +0200 Subject: [PATCH 012/256] Changed license verification code validation. --- packages/ckeditor5-core/src/editor/editor.ts | 18 ++++--------- .../ckeditor5-core/tests/editor/editor.js | 26 +++++++++---------- 2 files changed, 18 insertions(+), 26 deletions(-) diff --git a/packages/ckeditor5-core/src/editor/editor.ts b/packages/ckeditor5-core/src/editor/editor.ts index 8c1557755af..e2bdfc197a9 100644 --- a/packages/ckeditor5-core/src/editor/editor.ts +++ b/packages/ckeditor5-core/src/editor/editor.ts @@ -664,7 +664,7 @@ export default abstract class Editor extends ObservableMixin() { const licenseKey = this.config.get( 'licenseKey' ); if ( !licenseKey ) { - // TODO: For now, we don't block the editor if a licence key is not provided. + // TODO: For now, we don't block the editor if a licence key is not provided. GPL is assumed. return; } @@ -698,7 +698,7 @@ export default abstract class Editor extends ObservableMixin() { return; } - if ( crc32( getCrcInputData( licensePayload ) ) != licensePayload.verificationCode ) { + if ( crc32( getCrcInputData( licensePayload ) ) != licensePayload.vc ) { blockEditor( this, 'licenseFormatInvalid', 'The format of the license key is invalid.' ); return; @@ -740,7 +740,7 @@ export default abstract class Editor extends ObservableMixin() { } function hasAllRequiredFields( licensePayload: Record ) { - const requiredFields = [ 'exp', 'jti', 'verificationCode' ]; + const requiredFields = [ 'exp', 'jti', 'vc' ]; return requiredFields.every( field => field in licensePayload ); } @@ -749,18 +749,10 @@ export default abstract class Editor extends ObservableMixin() { * Returns an array of values that are used to calculate the CRC32 checksum. */ function getCrcInputData( licensePayload: Record ): CRCData { - const keysToCheck = [ - 'exp', - 'licensedHosts', - 'usageEndpoint', - 'distributionChannel', - 'whiteLabel', - 'licenseType', - 'features' - ]; + const keysToCheck = Object.getOwnPropertyNames( licensePayload ).sort(); const filteredValues = keysToCheck - .filter( key => licensePayload[ key ] !== undefined && licensePayload[ key ] !== null ) + .filter( key => key != 'vc' && licensePayload[ key ] != null ) .map( key => licensePayload[ key ] ); return [ ...filteredValues ] as CRCData; diff --git a/packages/ckeditor5-core/tests/editor/editor.js b/packages/ckeditor5-core/tests/editor/editor.js index 2b467c6f236..7c75c3b6434 100644 --- a/packages/ckeditor5-core/tests/editor/editor.js +++ b/packages/ckeditor5-core/tests/editor/editor.js @@ -225,7 +225,7 @@ describe( 'Editor', () => { describe( 'rquired fields in the license key', () => { it( 'should not block the editor when required fields are provided and are valid', () => { - const licenseKey = 'foo.eyJleHAiOjIyMDg5ODg4MDAsImp0aSI6ImZvbyIsInZlcmlmaWNhdGlvbkNvZGUiOiJjNTU2YWQ3NCJ9.bar'; + const licenseKey = 'foo.eyJleHAiOjIyMDg5ODg4MDAsImp0aSI6ImZvbyIsInZjIjoiNDYyYTkzMGQifQ.bar'; const editor = new TestEditor( { licenseKey } ); @@ -234,7 +234,7 @@ describe( 'Editor', () => { } ); it( 'should block the editor when the `exp` field is missing', () => { - const licenseKey = 'foo.eyJqdGkiOiJmb28iLCJ2ZXJpZmljYXRpb25Db2RlIjoiMCJ9.bar'; + const licenseKey = 'foo.eyJqdGkiOiJmb28iLCJ2YyI6IjhjNzM2NTIxIn0.bar'; const editor = new TestEditor( { licenseKey } ); sinon.assert.calledWithMatch( stub, 'The format of the license key is invalid.' ); @@ -242,7 +242,7 @@ describe( 'Editor', () => { } ); it( 'should block the editor when the `jti` field is missing', () => { - const licenseKey = 'foo.eyJleHAiOjIyMDg5ODg4MDAsInZlcmlmaWNhdGlvbkNvZGUiOiJjNTU2YWQ3NCJ9.bar'; + const licenseKey = 'foo.eyJleHAiOjIyMDg5ODg4MDAsInZjIjoiYzU1NmFkNzQifQ.bar'; const editor = new TestEditor( { licenseKey } ); @@ -250,8 +250,8 @@ describe( 'Editor', () => { expect( editor.isReadOnly ).to.be.true; } ); - it( 'should block the editor when the `verificationCode` field is missing', () => { - const licenseKey = 'foo.eyJleHAiOjIyMDg5ODg4MDAsImp0aSI6ImZvbyJ9.bar'; + it( 'should block the editor when the `vc` field is missing', () => { + const licenseKey = 'foo.eyJleHAiOjIyMDg5ODg4MDAsImp0aSI6ImZvbyJ9Cg.bar'; const editor = new TestEditor( { licenseKey } ); @@ -262,7 +262,7 @@ describe( 'Editor', () => { it( 'should block the editor when the license key is not valid (expiration date in the past)', () => { // eslint-disable-next-line max-len - const licenseKey = 'foo.eyJleHAiOjE3MDQwNjcyMDAsImp0aSI6ImZvbyIsInZlcmlmaWNhdGlvbkNvZGUiOiI2ZTlkNDM2NSJ9.bar'; + const licenseKey = 'foo.eyJleHAiOjE3MDQwNjcyMDAsImp0aSI6ImZvbyIsInZjIjoiOTc4NTlGQkIifQo.bar'; const editor = new TestEditor( { licenseKey } ); @@ -271,7 +271,7 @@ describe( 'Editor', () => { } ); it( 'should block the editor when the license key has wrong format (wrong verificationCode)', () => { - const licenseKey = 'foo.eyJleHAiOjIyMDg5ODg4MDAsImp0aSI6ImZvbyIsInZlcmlmaWNhdGlvbkNvZGUiOiIxMTExMTExMSJ9.bar'; + const licenseKey = 'foo.eyJleHAiOjIyMDg5ODg4MDAsImp0aSI6ImZvbyIsInZjIjoiMTIzNDU2NzgifQo.bar'; const editor = new TestEditor( { licenseKey } ); @@ -280,7 +280,7 @@ describe( 'Editor', () => { } ); it( 'should block the editor when the license key has wrong format (missing header part)', () => { - const licenseKey = 'eyJleHAiOjIyMDg5ODg4MDAsImp0aSI6ImZvbyIsInZlcmlmaWNhdGlvbkNvZGUiOiJjNTU2YWQ3NCJ9.bar'; + const licenseKey = 'eyJleHAiOjIyMDg5ODg4MDAsImp0aSI6ImZvbyIsInZjIjoiNDYyYTkzMGQifQ.bar'; const editor = new TestEditor( { licenseKey } ); @@ -312,7 +312,7 @@ describe( 'Editor', () => { const fetchStub = sinon.stub( window, 'fetch' ); // eslint-disable-next-line max-len - const licenseKey = 'foo.eyJleHAiOjM3ODY5MTIwMDAsImp0aSI6ImZvbyIsInVzYWdlRW5kcG9pbnQiOiJodHRwczovL2NrZWRpdG9yLmNvbSIsInZlcmlmaWNhdGlvbkNvZGUiOiJjZWE0ZjQ2MSJ9.bar'; + const licenseKey = 'foo.eyJleHAiOjM3ODY5MTIwMDAsImp0aSI6ImZvbyIsInVzYWdlRW5kcG9pbnQiOiJodHRwczovL2NrZWRpdG9yLmNvbSIsInZjIjoiYWI5NGFhZjYifQ.bar'; const editor = new TestEditor( { licenseKey } ); editor.fire( 'ready' ); @@ -329,7 +329,7 @@ describe( 'Editor', () => { const fetchStub = sinon.stub( window, 'fetch' ); // eslint-disable-next-line max-len - const licenseKey = 'foo.eyJleHAiOjM3ODY5MTIwMDAsImp0aSI6ImZvbyIsInZlcmlmaWNhdGlvbkNvZGUiOiI4ZjY3MzA0MCJ9.bar'; + const licenseKey = 'foo.eyJleHAiOjM3ODY5MTIwMDAsImp0aSI6ImZvbyIsInZjIjoiZjA3OTJhNjYifQ.bar'; const editor = new TestEditor( { licenseKey } ); editor.fire( 'ready' ); @@ -348,7 +348,7 @@ describe( 'Editor', () => { }; // eslint-disable-next-line max-len - const licenseKey = 'foo.eyJleHAiOjM3ODY5MTIwMDAsImp0aSI6ImZvbyIsInVzYWdlRW5kcG9pbnQiOiJodHRwczovL2NrZWRpdG9yLmNvbSIsInZlcmlmaWNhdGlvbkNvZGUiOiJjZWE0ZjQ2MSJ9.bar'; + const licenseKey = 'foo.eyJleHAiOjM3ODY5MTIwMDAsImp0aSI6ImZvbyIsInVzYWdlRW5kcG9pbnQiOiJodHRwczovL2NrZWRpdG9yLmNvbSIsInZjIjoiYWI5NGFhZjYifQ.bar'; const editor = new TestEditor( { licenseKey } ); editor.fire( 'ready' ); @@ -370,7 +370,7 @@ describe( 'Editor', () => { const warnStub = testUtils.sinon.stub( console, 'warn' ); // eslint-disable-next-line max-len - const licenseKey = 'foo.eyJleHAiOjM3ODY5MTIwMDAsImp0aSI6ImZvbyIsInVzYWdlRW5kcG9pbnQiOiJodHRwczovL2NrZWRpdG9yLmNvbSIsInZlcmlmaWNhdGlvbkNvZGUiOiJjZWE0ZjQ2MSJ9.bar'; + const licenseKey = 'foo.eyJleHAiOjM3ODY5MTIwMDAsImp0aSI6ImZvbyIsInVzYWdlRW5kcG9pbnQiOiJodHRwczovL2NrZWRpdG9yLmNvbSIsInZjIjoiYWI5NGFhZjYifQ.bar'; const editor = new TestEditor( { licenseKey } ); editor.fire( 'ready' ); @@ -393,7 +393,7 @@ describe( 'Editor', () => { const warnStub = testUtils.sinon.stub( console, 'warn' ); // eslint-disable-next-line max-len - const licenseKey = 'foo.eyJleHAiOjM3ODY5MTIwMDAsImp0aSI6ImZvbyIsInVzYWdlRW5kcG9pbnQiOiJodHRwczovL2NrZWRpdG9yLmNvbSIsInZlcmlmaWNhdGlvbkNvZGUiOiJjZWE0ZjQ2MSJ9.bar'; + const licenseKey = 'foo.eyJleHAiOjM3ODY5MTIwMDAsImp0aSI6ImZvbyIsInVzYWdlRW5kcG9pbnQiOiJodHRwczovL2NrZWRpdG9yLmNvbSIsInZjIjoiYWI5NGFhZjYifQ.bar'; const editor = new TestEditor( { licenseKey } ); editor.fire( 'ready' ); From e41a70fa52d07766491b81003c02b4fbc80b0985 Mon Sep 17 00:00:00 2001 From: Arkadiusz Filipczak Date: Mon, 6 May 2024 13:50:23 +0200 Subject: [PATCH 013/256] Added padding in crc32. --- packages/ckeditor5-utils/src/crc32.ts | 4 ++-- packages/ckeditor5-utils/tests/crc32.js | 16 +++++----------- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/packages/ckeditor5-utils/src/crc32.ts b/packages/ckeditor5-utils/src/crc32.ts index f4f114321de..096066cb8c1 100644 --- a/packages/ckeditor5-utils/src/crc32.ts +++ b/packages/ckeditor5-utils/src/crc32.ts @@ -48,7 +48,7 @@ function makeCrcTable(): Array { * If false, the checksum is returned as a numeric value. Default is true. * @returns The CRC-32 checksum, returned as a hexadecimal string by default, or as a number if returnHex is set to false. */ -export default function crc32( inputData: CRCData, returnHex: boolean = true ): number | string { +export default function crc32( inputData: CRCData ): string { const dataArray = Array.isArray( inputData ) ? inputData : [ inputData ]; const crcTable: Array = makeCrcTable(); let crc: number = 0 ^ ( -1 ); @@ -70,7 +70,7 @@ export default function crc32( inputData: CRCData, returnHex: boolean = true ): crc = ( crc ^ ( -1 ) ) >>> 0; // Force unsigned integer - return returnHex ? crc.toString( 16 ) : crc; + return crc.toString( 16 ).padStart( 8, '0' ); } /** diff --git a/packages/ckeditor5-utils/tests/crc32.js b/packages/ckeditor5-utils/tests/crc32.js index 03fa82a5a35..b263fa85cca 100644 --- a/packages/ckeditor5-utils/tests/crc32.js +++ b/packages/ckeditor5-utils/tests/crc32.js @@ -27,7 +27,7 @@ describe( 'crc32', () => { it( 'should correctly calculate the CRC32 checksum for an empty string', () => { const input = ''; - const expectedHex = '0'; + const expectedHex = '00000000'; expect( crc32( input ) ).to.equal( expectedHex ); } ); } ); @@ -65,24 +65,18 @@ describe( 'crc32', () => { it( 'should correctly handle an empty array', () => { const input = []; - const expectedHex = '0'; + const expectedHex = '00000000'; expect( crc32( input ) ).to.equal( expectedHex ); } ); it( 'should correctly handle arrays containing empty strings', () => { const input = [ '', '', '' ]; - const expectedHex = '0'; + const expectedHex = '00000000'; expect( crc32( input ) ).to.equal( expectedHex ); } ); } ); describe( 'return values', () => { - it( 'should return a number when returnHex is set to false', () => { - const input = [ 'foo' ]; - const result = 2356372769; - expect( crc32( input, false ) ).to.equal( result ); - } ); - it( 'should return a hexadecimal string when returnHex is true', () => { const input = [ 'foo' ]; const result = '8c736521'; @@ -97,8 +91,8 @@ describe( 'crc32', () => { it( 'should return consistent results for the same input', () => { const input = [ 'foo', 'bar' ]; - const firstRun = crc32( input, true ); - const secondRun = crc32( input, true ); + const firstRun = crc32( input ); + const secondRun = crc32( input ); expect( firstRun ).to.equal( secondRun ); } ); } ); From 93e942bf668f87a6b12376485c14e4ddb80b5ecf Mon Sep 17 00:00:00 2001 From: Arkadiusz Filipczak Date: Mon, 6 May 2024 13:54:22 +0200 Subject: [PATCH 014/256] Fixed api docs for crc32. --- packages/ckeditor5-utils/src/crc32.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/ckeditor5-utils/src/crc32.ts b/packages/ckeditor5-utils/src/crc32.ts index 096066cb8c1..e5d7242e0dc 100644 --- a/packages/ckeditor5-utils/src/crc32.ts +++ b/packages/ckeditor5-utils/src/crc32.ts @@ -44,9 +44,7 @@ function makeCrcTable(): Array { * * `crc32(['foo', 123, true])` produces the same result as `crc32('foo123true')` * * Nested arrays of strings are flattened, so `crc32([['foo', 'bar'], 'baz'])` is equivalent to `crc32(['foobar', 'baz'])` * - * @param returnHex Optional. Specifies the format of the return value. If set to true, the checksum is returned as a hexadecimal string. - * If false, the checksum is returned as a numeric value. Default is true. - * @returns The CRC-32 checksum, returned as a hexadecimal string by default, or as a number if returnHex is set to false. + * @returns The CRC-32 checksum, returned as a hexadecimal string. */ export default function crc32( inputData: CRCData ): string { const dataArray = Array.isArray( inputData ) ? inputData : [ inputData ]; From c515adefa5ef54fdbbb54bb058e677b4e4f3aae2 Mon Sep 17 00:00:00 2001 From: Illia Sheremetov Date: Tue, 7 May 2024 15:32:05 +0200 Subject: [PATCH 015/256] Implement domain check logic. --- packages/ckeditor5-core/src/editor/editor.ts | 26 +++ .../ckeditor5-core/tests/editor/editor.js | 182 +++++++++++++++++- 2 files changed, 207 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5-core/src/editor/editor.ts b/packages/ckeditor5-core/src/editor/editor.ts index e2bdfc197a9..f22dc87a0c3 100644 --- a/packages/ckeditor5-core/src/editor/editor.ts +++ b/packages/ckeditor5-core/src/editor/editor.ts @@ -704,6 +704,24 @@ export default abstract class Editor extends ObservableMixin() { return; } + const licensedHosts: Array = licensePayload.licensedHosts; + + if ( licensedHosts ) { + const hostname = this._getHostname(); + const willcards = licensedHosts + .filter( val => val.slice( 0, 2 ) === '*.' ) + .map( val => val.slice( 1 ) ); + + const isHostnameMatched = licensedHosts.some( licensedHost => licensedHost === hostname ); + const isWillcardMatched = willcards.some( willcard => willcard === hostname.slice( -willcard.length ) ); + + if ( !isWillcardMatched && !isHostnameMatched ) { + blockEditor( this, 'licenseFormatInvalid', `Domain "${ hostname }" does not have access to the provided license.` ); + + return; + } + } + if ( licensePayload.usageEndpoint ) { this.once( 'ready', () => { const telemetryData = this._getTelemetryData(); @@ -759,6 +777,14 @@ export default abstract class Editor extends ObservableMixin() { } } + /** + * Returns hostname of current page. Created for testing purpose, because + * window.location.hostname cannot be stubbed by sinon. + */ + private _getHostname() { + return window.location.hostname; + } + private async _sendUsageRequest( endpoint: string, licenseKey: string, diff --git a/packages/ckeditor5-core/tests/editor/editor.js b/packages/ckeditor5-core/tests/editor/editor.js index 7c75c3b6434..270bcb27703 100644 --- a/packages/ckeditor5-core/tests/editor/editor.js +++ b/packages/ckeditor5-core/tests/editor/editor.js @@ -223,7 +223,7 @@ describe( 'Editor', () => { stub = testUtils.sinon.stub( console, 'warn' ); } ); - describe( 'rquired fields in the license key', () => { + describe( 'required fields in the license key', () => { it( 'should not block the editor when required fields are provided and are valid', () => { const licenseKey = 'foo.eyJleHAiOjIyMDg5ODg4MDAsImp0aSI6ImZvbyIsInZjIjoiNDYyYTkzMGQifQ.bar'; @@ -260,6 +260,186 @@ describe( 'Editor', () => { } ); } ); + describe( 'domain check', () => { + it( 'should pass when licensedHosts list is not defined', () => { + const licenseKey = 'foo.eyJleHAiOjE3MTc1NDU2MDAsImp0aSI6IjliY' + + 'WYwOTljLWNkODMtNDJmZi05NzlmLTYyYmYxYzMyOGFkYiIsInZjIjoiZTU2NDMwZGEifQ.bar'; + + /** + * after decoding licenseKey: + * + * licensePaylod: { + * ..., + * licensedHosts: undefined + * } + */ + + const hostnameStub = sinon.stub( TestEditor.prototype, '_getHostname' ).returns( 'localhost' ); + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.notCalled( stub ); + expect( editor.isReadOnly ).to.be.false; + + hostnameStub.restore(); + } ); + + it( 'should pass when localhost is in the licensedHosts list', () => { + const licenseKey = 'foo.eyJleHAiOjE3MTc1NDU2MDAsImp0aSI6Ijg0YWY4MjU4LTkxOTUtN' + + 'DllMy1iYzRhLTkwMWIzOTJmNGQ4ZiIsImxpY2Vuc2VkSG9zdHMiOlsibG9jYWxob3N0Il0sInZjIjoiYjY0ZjAwYmQifQ.bar'; + + /** + * after decoding licenseKey: + * + * licensePaylod: { + * ..., + * licensedHosts: [ 'localhost' ] + * } + */ + + const hostnameStub = sinon.stub( TestEditor.prototype, '_getHostname' ).returns( 'localhost' ); + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.notCalled( stub ); + expect( editor.isReadOnly ).to.be.false; + + hostnameStub.restore(); + } ); + + it( 'should not pass when localhost is not in the licensedHosts list', () => { + const licenseKey = 'foo.eyJleHAiOjE3MTc1NDU2MDAsImp0aSI6Ijc2MWI4ZWQ2LWRmZTAtNGY0OS1hMTRkLWU2YzkxZjA4Y2ZjZSIsIm' + + 'xpY2Vuc2VkSG9zdHMiOlsiZmFjZWJvb2suY29tIl0sInZjIjoiNmEzNDdmYzYifQ.bar'; + + /** + * after decoding licenseKey: + * + * licensePaylod: { + * ..., + * licensedHosts: [ 'facebook.com' ] + * } + */ + + const hostnameStub = sinon.stub( TestEditor.prototype, '_getHostname' ).returns( 'localhost' ); + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.calledWithMatch( stub, 'Domain "localhost" does not have access to the provided license.' ); + expect( editor.isReadOnly ).to.be.true; + + hostnameStub.restore(); + } ); + + it( 'should not pass when domain is not in the licensedHosts list', () => { + const licenseKey = 'foo.eyJleHAiOjE3MTc1NDU2MDAsImp0aSI6Ijc2MWI4ZWQ2LWRmZTAtNGY0OS1hMTRkLWU2YzkxZjA4Y2ZjZSIsIm' + + 'xpY2Vuc2VkSG9zdHMiOlsiZmFjZWJvb2suY29tIl0sInZjIjoiNmEzNDdmYzYifQ.bar'; + + /** + * after decoding licenseKey: + * + * licensePaylod: { + * ..., + * licensedHosts: [ 'facebook.com' ] + * } + */ + + const hostnameStub = sinon.stub( TestEditor.prototype, '_getHostname' ).returns( 'notion.so' ); + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.calledWithMatch( stub, 'Domain "notion.so" does not have access to the provided license.' ); + expect( editor.isReadOnly ).to.be.true; + + hostnameStub.restore(); + } ); + + it( 'should pass when domain is in the licensedHosts list', () => { + const licenseKey = 'foo.eyJleHAiOjE3MTc1NDU2MDAsImp0aSI6Ijc2MWI4ZWQ2LWRmZTAtNGY0OS1hMTRkLWU2YzkxZjA4Y2ZjZSIsIm' + + 'xpY2Vuc2VkSG9zdHMiOlsiZmFjZWJvb2suY29tIl0sInZjIjoiNmEzNDdmYzYifQ.bar'; + + /** + * after decoding licenseKey: + * + * licensePaylod: { + * ..., + * licensedHosts: [ 'facebook.com' ] + * } + */ + + const hostnameStub = sinon.stub( TestEditor.prototype, '_getHostname' ).returns( 'facebook.com' ); + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.notCalled( stub ); + expect( editor.isReadOnly ).to.be.false; + + hostnameStub.restore(); + } ); + + describe( 'willcards', () => { + it( 'should pass when matched willcard from the licensedHosts list', () => { + const licenseKey = 'foo.eyJleHAiOjE3MTc1NDU2MDAsImp0aSI6IjBjZjc2MGE1LTMyYzQtNDIzMC04ZjQ3LTJmN2Q1NzBkMjk5NSIsIm' + + 'xpY2Vuc2VkSG9zdHMiOlsiKi5ub3Rpb24uc28iXSwidmMiOiIxMGE1ODcwMiJ9.bar'; + + /** + * after decoding licenseKey: + * + * licensePaylod: { + * ..., + * licensedHosts: [ '*.notion.so' ] + * } + */ + + const hostnameStub = sinon.stub( TestEditor.prototype, '_getHostname' ).returns( 'subdomen.notion.so' ); + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.notCalled( stub ); + expect( editor.isReadOnly ).to.be.false; + + hostnameStub.restore(); + } ); + + it( 'should not pass if not matched willcard from the licensedHosts list', () => { + const licenseKey = 'foo.eyJleHAiOjE3MTc1NDU2MDAsImp0aSI6IjBjZjc2MGE1LTMyYzQtNDIzMC04ZjQ3LTJmN2Q1NzBkMjk5NSIsIm' + + 'xpY2Vuc2VkSG9zdHMiOlsiKi5ub3Rpb24uc28iXSwidmMiOiIxMGE1ODcwMiJ9.bar'; + + /** + * after decoding licenseKey: + * + * licensePaylod: { + * ..., + * licensedHosts: [ '*.notion.so' ] + * } + */ + + const hostnameStub = sinon.stub( TestEditor.prototype, '_getHostname' ).returns( 'subdomen.nnotion.so' ); + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.calledWithMatch( stub, 'Domain "subdomen.nnotion.so" does not have access to the provided license.' ); + expect( editor.isReadOnly ).to.be.true; + + hostnameStub.restore(); + } ); + + it( 'should not pass if domain have no subdomen', () => { + const licenseKey = 'foo.eyJleHAiOjE3MTc1NDU2MDAsImp0aSI6IjBjZjc2MGE1LTMyYzQtNDIzMC04ZjQ3LTJmN2Q1NzBkMjk5NSIsIm' + + 'xpY2Vuc2VkSG9zdHMiOlsiKi5ub3Rpb24uc28iXSwidmMiOiIxMGE1ODcwMiJ9.bar'; + + /** + * after decoding licenseKey: + * + * licensePaylod: { + * ..., + * licensedHosts: [ '*.notion.so' ] + * } + */ + + const hostnameStub = sinon.stub( TestEditor.prototype, '_getHostname' ).returns( 'notion.so' ); + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.calledWithMatch( stub, 'Domain "notion.so" does not have access to the provided license.' ); + expect( editor.isReadOnly ).to.be.true; + + hostnameStub.restore(); + } ); + } ); + } ); + it( 'should block the editor when the license key is not valid (expiration date in the past)', () => { // eslint-disable-next-line max-len const licenseKey = 'foo.eyJleHAiOjE3MDQwNjcyMDAsImp0aSI6ImZvbyIsInZjIjoiOTc4NTlGQkIifQo.bar'; From 10eeeb76e6eda7e33feded3834523b9ee134a9c3 Mon Sep 17 00:00:00 2001 From: Illia Sheremetov Date: Tue, 7 May 2024 15:34:09 +0200 Subject: [PATCH 016/256] Fix typo. --- packages/ckeditor5-core/tests/editor/editor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ckeditor5-core/tests/editor/editor.js b/packages/ckeditor5-core/tests/editor/editor.js index 270bcb27703..3036dc0e382 100644 --- a/packages/ckeditor5-core/tests/editor/editor.js +++ b/packages/ckeditor5-core/tests/editor/editor.js @@ -416,7 +416,7 @@ describe( 'Editor', () => { hostnameStub.restore(); } ); - it( 'should not pass if domain have no subdomen', () => { + it( 'should not pass if domain have no subdomain', () => { const licenseKey = 'foo.eyJleHAiOjE3MTc1NDU2MDAsImp0aSI6IjBjZjc2MGE1LTMyYzQtNDIzMC04ZjQ3LTJmN2Q1NzBkMjk5NSIsIm' + 'xpY2Vuc2VkSG9zdHMiOlsiKi5ub3Rpb24uc28iXSwidmMiOiIxMGE1ODcwMiJ9.bar'; From eaf45a58c7c514547bfe885b7344bd63f770805b Mon Sep 17 00:00:00 2001 From: Illia Sheremetov Date: Tue, 7 May 2024 16:17:13 +0200 Subject: [PATCH 017/256] Back cc to 100%. --- packages/ckeditor5-core/tests/editor/editor.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/ckeditor5-core/tests/editor/editor.js b/packages/ckeditor5-core/tests/editor/editor.js index 3036dc0e382..560393ea3b7 100644 --- a/packages/ckeditor5-core/tests/editor/editor.js +++ b/packages/ckeditor5-core/tests/editor/editor.js @@ -296,13 +296,10 @@ describe( 'Editor', () => { * } */ - const hostnameStub = sinon.stub( TestEditor.prototype, '_getHostname' ).returns( 'localhost' ); const editor = new TestEditor( { licenseKey } ); sinon.assert.notCalled( stub ); expect( editor.isReadOnly ).to.be.false; - - hostnameStub.restore(); } ); it( 'should not pass when localhost is not in the licensedHosts list', () => { From 080745c71e9fa87cd6035351c7ecb1fef1f48550 Mon Sep 17 00:00:00 2001 From: Illia Sheremetov Date: Wed, 8 May 2024 14:43:04 +0200 Subject: [PATCH 018/256] Fix error name. --- packages/ckeditor5-core/src/editor/editor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ckeditor5-core/src/editor/editor.ts b/packages/ckeditor5-core/src/editor/editor.ts index f22dc87a0c3..5678a077821 100644 --- a/packages/ckeditor5-core/src/editor/editor.ts +++ b/packages/ckeditor5-core/src/editor/editor.ts @@ -716,7 +716,7 @@ export default abstract class Editor extends ObservableMixin() { const isWillcardMatched = willcards.some( willcard => willcard === hostname.slice( -willcard.length ) ); if ( !isWillcardMatched && !isHostnameMatched ) { - blockEditor( this, 'licenseFormatInvalid', `Domain "${ hostname }" does not have access to the provided license.` ); + blockEditor( this, 'licenseHostInvalid', `Domain "${ hostname }" does not have access to the provided license.` ); return; } From fd55448a3950cb79eb14a2ec083372bc4b673a79 Mon Sep 17 00:00:00 2001 From: Arkadiusz Filipczak Date: Thu, 9 May 2024 11:11:33 +0200 Subject: [PATCH 019/256] Showing license errors refactored. --- packages/ckeditor5-core/src/editor/editor.ts | 43 +++++++++++++------ .../ckeditor5-core/tests/editor/editor.js | 37 ++++++++-------- 2 files changed, 48 insertions(+), 32 deletions(-) diff --git a/packages/ckeditor5-core/src/editor/editor.ts b/packages/ckeditor5-core/src/editor/editor.ts index e2bdfc197a9..1154b6b12bd 100644 --- a/packages/ckeditor5-core/src/editor/editor.ts +++ b/packages/ckeditor5-core/src/editor/editor.ts @@ -671,7 +671,7 @@ export default abstract class Editor extends ObservableMixin() { const encodedPayload = getPayload( licenseKey ); if ( !encodedPayload ) { - blockEditor( this, 'licenseFormatInvalid', 'The format of the license key is invalid.' ); + blockEditor( this, 'invalid' ); return; } @@ -679,27 +679,27 @@ export default abstract class Editor extends ObservableMixin() { const licensePayload = parseBase64EncodedObject( encodedPayload ); if ( !licensePayload ) { - blockEditor( this, 'licenseFormatInvalid', 'The format of the license key is invalid.' ); + blockEditor( this, 'invalid' ); return; } if ( !hasAllRequiredFields( licensePayload ) ) { - blockEditor( this, 'licenseFormatInvalid', 'The format of the license key is invalid.' ); + blockEditor( this, 'invalid' ); return; } - const expirationDate = new Date( licensePayload.exp * 1000 ); - - if ( expirationDate < releaseDate ) { - blockEditor( this, 'licenseExpired', 'The validation period for the editor license key has expired.' ); + if ( crc32( getCrcInputData( licensePayload ) ) != licensePayload.vc.toLowerCase() ) { + blockEditor( this, 'invalid' ); return; } - if ( crc32( getCrcInputData( licensePayload ) ) != licensePayload.vc ) { - blockEditor( this, 'licenseFormatInvalid', 'The format of the license key is invalid.' ); + const expirationDate = new Date( licensePayload.exp * 1000 ); + + if ( expirationDate < releaseDate ) { + blockEditor( this, 'expired' ); return; } @@ -716,8 +716,7 @@ export default abstract class Editor extends ObservableMixin() { } if ( status != 'ok' ) { - // TODO: check if this message is ok here. - blockEditor( this, 'usageExceeded', 'The licensed usage count exceeded' ); + blockEditor( this, 'usageLimit' ); } } ); }, { priority: 'high' } ); @@ -733,10 +732,10 @@ export default abstract class Editor extends ObservableMixin() { return parts[ 1 ]; } - function blockEditor( editor: Editor, reason: string, message: string ) { - console.warn( message ); + function blockEditor( editor: Editor, reason: LicenseErrorReason ) { + editor._showLicenseError( reason ); - editor.enableReadOnlyMode( reason ); + editor.enableReadOnlyMode( Symbol( 'invalidLicense' ) ); } function hasAllRequiredFields( licensePayload: Record ) { @@ -759,6 +758,20 @@ export default abstract class Editor extends ObservableMixin() { } } + /* istanbul ignore next -- @preserve */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + private _showLicenseError( reason: LicenseErrorReason, featureName?: string ) { + // Make sure the error thrown is unhandled. + setTimeout( () => { + /** + * TODO: consider error kinds for each reason. + * + * @error todo-specify-this-error-code + */ + throw new CKEditorError( 'todo-specify-this-error-code', null ); + }, 0 ); + } + private async _sendUsageRequest( endpoint: string, licenseKey: string, @@ -785,6 +798,8 @@ export default abstract class Editor extends ObservableMixin() { } } +type LicenseErrorReason = 'invalid' | 'expired' | 'domainLimit' | 'featureNotAllowed' | 'trialLimit' | 'developmentLimit' | 'usageLimit'; + /** * Fired when the {@link module:engine/controller/datacontroller~DataController#event:ready data} and all additional * editor components are ready. diff --git a/packages/ckeditor5-core/tests/editor/editor.js b/packages/ckeditor5-core/tests/editor/editor.js index 7c75c3b6434..53c192c3c4d 100644 --- a/packages/ckeditor5-core/tests/editor/editor.js +++ b/packages/ckeditor5-core/tests/editor/editor.js @@ -217,10 +217,10 @@ describe( 'Editor', () => { } ); describe( 'license key verification', () => { - let stub; + let showErrorStub; beforeEach( () => { - stub = testUtils.sinon.stub( console, 'warn' ); + showErrorStub = testUtils.sinon.stub( TestEditor.prototype, '_showLicenseError' ); } ); describe( 'rquired fields in the license key', () => { @@ -229,7 +229,7 @@ describe( 'Editor', () => { const editor = new TestEditor( { licenseKey } ); - sinon.assert.notCalled( stub ); + sinon.assert.notCalled( showErrorStub ); expect( editor.isReadOnly ).to.be.false; } ); @@ -237,7 +237,7 @@ describe( 'Editor', () => { const licenseKey = 'foo.eyJqdGkiOiJmb28iLCJ2YyI6IjhjNzM2NTIxIn0.bar'; const editor = new TestEditor( { licenseKey } ); - sinon.assert.calledWithMatch( stub, 'The format of the license key is invalid.' ); + sinon.assert.calledWithMatch( showErrorStub, 'invalid' ); expect( editor.isReadOnly ).to.be.true; } ); @@ -246,7 +246,7 @@ describe( 'Editor', () => { const editor = new TestEditor( { licenseKey } ); - sinon.assert.calledWithMatch( stub, 'The format of the license key is invalid.' ); + sinon.assert.calledWithMatch( showErrorStub, 'invalid' ); expect( editor.isReadOnly ).to.be.true; } ); @@ -255,18 +255,17 @@ describe( 'Editor', () => { const editor = new TestEditor( { licenseKey } ); - sinon.assert.calledWithMatch( stub, 'The format of the license key is invalid.' ); + sinon.assert.calledWithMatch( showErrorStub, 'invalid' ); expect( editor.isReadOnly ).to.be.true; } ); } ); it( 'should block the editor when the license key is not valid (expiration date in the past)', () => { - // eslint-disable-next-line max-len const licenseKey = 'foo.eyJleHAiOjE3MDQwNjcyMDAsImp0aSI6ImZvbyIsInZjIjoiOTc4NTlGQkIifQo.bar'; const editor = new TestEditor( { licenseKey } ); - sinon.assert.calledWithMatch( stub, 'The validation period for the editor license key has expired.' ); + sinon.assert.calledWithMatch( showErrorStub, 'expired' ); expect( editor.isReadOnly ).to.be.true; } ); @@ -275,7 +274,7 @@ describe( 'Editor', () => { const editor = new TestEditor( { licenseKey } ); - sinon.assert.calledWithMatch( stub, 'The format of the license key is invalid.' ); + sinon.assert.calledWithMatch( showErrorStub, 'invalid' ); expect( editor.isReadOnly ).to.be.true; } ); @@ -284,7 +283,7 @@ describe( 'Editor', () => { const editor = new TestEditor( { licenseKey } ); - sinon.assert.calledWithMatch( stub, 'The format of the license key is invalid.' ); + sinon.assert.calledWithMatch( showErrorStub, 'invalid' ); expect( editor.isReadOnly ).to.be.true; } ); @@ -293,7 +292,7 @@ describe( 'Editor', () => { const editor = new TestEditor( { licenseKey } ); - sinon.assert.calledWithMatch( stub, 'The format of the license key is invalid.' ); + sinon.assert.calledWithMatch( showErrorStub, 'invalid' ); expect( editor.isReadOnly ).to.be.true; } ); @@ -302,7 +301,7 @@ describe( 'Editor', () => { const editor = new TestEditor( { licenseKey } ); - sinon.assert.calledWithMatch( stub, 'The format of the license key is invalid.' ); + sinon.assert.calledWithMatch( showErrorStub, 'invalid' ); expect( editor.isReadOnly ).to.be.true; } ); } ); @@ -367,7 +366,7 @@ describe( 'Editor', () => { status: 'foo' } ) } ); - const warnStub = testUtils.sinon.stub( console, 'warn' ); + const showErrorStub = testUtils.sinon.stub( TestEditor.prototype, '_showLicenseError' ); // eslint-disable-next-line max-len const licenseKey = 'foo.eyJleHAiOjM3ODY5MTIwMDAsImp0aSI6ImZvbyIsInVzYWdlRW5kcG9pbnQiOiJodHRwczovL2NrZWRpdG9yLmNvbSIsInZjIjoiYWI5NGFhZjYifQ.bar'; @@ -377,8 +376,8 @@ describe( 'Editor', () => { await wait( 1 ); sinon.assert.calledOnce( fetchStub ); - sinon.assert.calledOnce( warnStub ); - sinon.assert.calledWithMatch( warnStub, 'The licensed usage count exceeded' ); + sinon.assert.calledOnce( showErrorStub ); + sinon.assert.calledWithMatch( showErrorStub, 'usageLimit' ); expect( editor.isReadOnly ).to.be.true; } ); @@ -391,6 +390,7 @@ describe( 'Editor', () => { } ) } ); const warnStub = testUtils.sinon.stub( console, 'warn' ); + const showErrorStub = testUtils.sinon.stub( TestEditor.prototype, '_showLicenseError' ); // eslint-disable-next-line max-len const licenseKey = 'foo.eyJleHAiOjM3ODY5MTIwMDAsImp0aSI6ImZvbyIsInVzYWdlRW5kcG9pbnQiOiJodHRwczovL2NrZWRpdG9yLmNvbSIsInZjIjoiYWI5NGFhZjYifQ.bar'; @@ -400,9 +400,10 @@ describe( 'Editor', () => { await wait( 1 ); sinon.assert.calledOnce( fetchStub ); - sinon.assert.calledTwice( warnStub ); - sinon.assert.calledWithMatch( warnStub.getCall( 0 ), 'bar' ); - sinon.assert.calledWithMatch( warnStub.getCall( 1 ), 'The licensed usage count exceeded' ); + sinon.assert.calledOnce( warnStub ); + sinon.assert.calledOnce( showErrorStub ); + sinon.assert.calledWithMatch( warnStub, 'bar' ); + sinon.assert.calledWithMatch( showErrorStub, 'usageLimit' ); expect( editor.isReadOnly ).to.be.true; } ); } ); From 9f4ddd88dc2c0c6ea47f87f6610ead24ae9738eb Mon Sep 17 00:00:00 2001 From: Illia Sheremetov Date: Thu, 9 May 2024 13:02:23 +0200 Subject: [PATCH 020/256] Inline _getHostname method. --- packages/ckeditor5-core/src/editor/editor.ts | 2 +- .../ckeditor5-core/tests/editor/editor.js | 133 +----------------- 2 files changed, 8 insertions(+), 127 deletions(-) diff --git a/packages/ckeditor5-core/src/editor/editor.ts b/packages/ckeditor5-core/src/editor/editor.ts index a3ffdaa6b2b..dbbd6c0f1d6 100644 --- a/packages/ckeditor5-core/src/editor/editor.ts +++ b/packages/ckeditor5-core/src/editor/editor.ts @@ -709,7 +709,7 @@ export default abstract class Editor extends ObservableMixin() { if ( licensedHosts ) { const hostname = window.location.hostname; const willcards = licensedHosts - .filter( val => val.slice( 0, 2 ) === '*.' ) + .filter( val => val.slice( 0, 1 ) === '*' ) .map( val => val.slice( 1 ) ); const isHostnameMatched = licensedHosts.some( licensedHost => licensedHost === hostname ); diff --git a/packages/ckeditor5-core/tests/editor/editor.js b/packages/ckeditor5-core/tests/editor/editor.js index b7ed1a050cd..3749e2bea3c 100644 --- a/packages/ckeditor5-core/tests/editor/editor.js +++ b/packages/ckeditor5-core/tests/editor/editor.js @@ -261,28 +261,6 @@ describe( 'Editor', () => { } ); describe( 'domain check', () => { - it( 'should pass when licensedHosts list is not defined', () => { - const licenseKey = 'foo.eyJleHAiOjE3MTc1NDU2MDAsImp0aSI6IjliY' + - 'WYwOTljLWNkODMtNDJmZi05NzlmLTYyYmYxYzMyOGFkYiIsInZjIjoiZTU2NDMwZGEifQ.bar'; - - /** - * after decoding licenseKey: - * - * licensePaylod: { - * ..., - * licensedHosts: undefined - * } - */ - - const hostnameStub = sinon.stub( TestEditor.prototype, '_getHostname' ).returns( 'localhost' ); - const editor = new TestEditor( { licenseKey } ); - - sinon.assert.notCalled( showErrorStub ); - expect( editor.isReadOnly ).to.be.false; - - hostnameStub.restore(); - } ); - it( 'should pass when localhost is in the licensedHosts list', () => { const licenseKey = 'foo.eyJleHAiOjE3MTc1NDU2MDAsImp0aSI6Ijg0YWY4MjU4LTkxOTUtN' + 'DllMy1iYzRhLTkwMWIzOTJmNGQ4ZiIsImxpY2Vuc2VkSG9zdHMiOlsibG9jYWxob3N0Il0sInZjIjoiYjY0ZjAwYmQifQ.bar'; @@ -302,28 +280,6 @@ describe( 'Editor', () => { expect( editor.isReadOnly ).to.be.false; } ); - it( 'should not pass when localhost is not in the licensedHosts list', () => { - const licenseKey = 'foo.eyJleHAiOjE3MTc1NDU2MDAsImp0aSI6Ijc2MWI4ZWQ2LWRmZTAtNGY0OS1hMTRkLWU2YzkxZjA4Y2ZjZSIsIm' + - 'xpY2Vuc2VkSG9zdHMiOlsiZmFjZWJvb2suY29tIl0sInZjIjoiNmEzNDdmYzYifQ.bar'; - - /** - * after decoding licenseKey: - * - * licensePaylod: { - * ..., - * licensedHosts: [ 'facebook.com' ] - * } - */ - - const hostnameStub = sinon.stub( TestEditor.prototype, '_getHostname' ).returns( 'localhost' ); - const editor = new TestEditor( { licenseKey } ); - - sinon.assert.calledWithMatch( showErrorStub, 'Domain "localhost" does not have access to the provided license.' ); - expect( editor.isReadOnly ).to.be.true; - - hostnameStub.restore(); - } ); - it( 'should not pass when domain is not in the licensedHosts list', () => { const licenseKey = 'foo.eyJleHAiOjE3MTc1NDU2MDAsImp0aSI6Ijc2MWI4ZWQ2LWRmZTAtNGY0OS1hMTRkLWU2YzkxZjA4Y2ZjZSIsIm' + 'xpY2Vuc2VkSG9zdHMiOlsiZmFjZWJvb2suY29tIl0sInZjIjoiNmEzNDdmYzYifQ.bar'; @@ -337,104 +293,29 @@ describe( 'Editor', () => { * } */ - const hostnameStub = sinon.stub( TestEditor.prototype, '_getHostname' ).returns( 'notion.so' ); const editor = new TestEditor( { licenseKey } ); - sinon.assert.calledWithMatch( showErrorStub, 'Domain "notion.so" does not have access to the provided license.' ); + sinon.assert.calledWithMatch( showErrorStub, 'invalid' ); expect( editor.isReadOnly ).to.be.true; - - hostnameStub.restore(); } ); - it( 'should pass when domain is in the licensedHosts list', () => { - const licenseKey = 'foo.eyJleHAiOjE3MTc1NDU2MDAsImp0aSI6Ijc2MWI4ZWQ2LWRmZTAtNGY0OS1hMTRkLWU2YzkxZjA4Y2ZjZSIsIm' + - 'xpY2Vuc2VkSG9zdHMiOlsiZmFjZWJvb2suY29tIl0sInZjIjoiNmEzNDdmYzYifQ.bar'; + it( 'should not pass if domain have no subdomain', () => { + const licenseKey = 'foo.eyJleHAiOjE3MTUzODU2MDAsImp0aSI6IjZmZGIxN2RhLTBiODAtNDI2Yi05ODA0LTc0NTEyNTZjMWE5N' + + 'yIsImxpY2Vuc2VkSG9zdHMiOlsiKi5sb2NhbGhvc3QiXSwidmMiOiJjNDMzYTk4OSJ9.bar'; /** * after decoding licenseKey: * * licensePaylod: { * ..., - * licensedHosts: [ 'facebook.com' ] + * licensedHosts: [ '*.localhost' ] * } */ - const hostnameStub = sinon.stub( TestEditor.prototype, '_getHostname' ).returns( 'facebook.com' ); const editor = new TestEditor( { licenseKey } ); - sinon.assert.notCalled( showErrorStub ); - expect( editor.isReadOnly ).to.be.false; - - hostnameStub.restore(); - } ); - - describe( 'willcards', () => { - it( 'should pass when matched willcard from the licensedHosts list', () => { - const licenseKey = 'foo.eyJleHAiOjE3MTc1NDU2MDAsImp0aSI6IjBjZjc2MGE1LTMyYzQtNDIzMC04ZjQ3LTJmN2Q1NzBkMjk5NSIsIm' + - 'xpY2Vuc2VkSG9zdHMiOlsiKi5ub3Rpb24uc28iXSwidmMiOiIxMGE1ODcwMiJ9.bar'; - - /** - * after decoding licenseKey: - * - * licensePaylod: { - * ..., - * licensedHosts: [ '*.notion.so' ] - * } - */ - - const hostnameStub = sinon.stub( TestEditor.prototype, '_getHostname' ).returns( 'subdomen.notion.so' ); - const editor = new TestEditor( { licenseKey } ); - - sinon.assert.notCalled( showErrorStub ); - expect( editor.isReadOnly ).to.be.false; - - hostnameStub.restore(); - } ); - - it( 'should not pass if not matched willcard from the licensedHosts list', () => { - const licenseKey = 'foo.eyJleHAiOjE3MTc1NDU2MDAsImp0aSI6IjBjZjc2MGE1LTMyYzQtNDIzMC04ZjQ3LTJmN2Q1NzBkMjk5NSIsIm' + - 'xpY2Vuc2VkSG9zdHMiOlsiKi5ub3Rpb24uc28iXSwidmMiOiIxMGE1ODcwMiJ9.bar'; - - /** - * after decoding licenseKey: - * - * licensePaylod: { - * ..., - * licensedHosts: [ '*.notion.so' ] - * } - */ - - const hostnameStub = sinon.stub( TestEditor.prototype, '_getHostname' ).returns( 'subdomen.nnotion.so' ); - const editor = new TestEditor( { licenseKey } ); - - sinon.assert.calledWithMatch( showErrorStub, - 'Domain "subdomen.nnotion.so" does not have access to the provided license.' ); - expect( editor.isReadOnly ).to.be.true; - - hostnameStub.restore(); - } ); - - it( 'should not pass if domain have no subdomain', () => { - const licenseKey = 'foo.eyJleHAiOjE3MTc1NDU2MDAsImp0aSI6IjBjZjc2MGE1LTMyYzQtNDIzMC04ZjQ3LTJmN2Q1NzBkMjk5NSIsIm' + - 'xpY2Vuc2VkSG9zdHMiOlsiKi5ub3Rpb24uc28iXSwidmMiOiIxMGE1ODcwMiJ9.bar'; - - /** - * after decoding licenseKey: - * - * licensePaylod: { - * ..., - * licensedHosts: [ '*.notion.so' ] - * } - */ - - const hostnameStub = sinon.stub( TestEditor.prototype, '_getHostname' ).returns( 'notion.so' ); - const editor = new TestEditor( { licenseKey } ); - - sinon.assert.calledWithMatch( showErrorStub, 'Domain "notion.so" does not have access to the provided license.' ); - expect( editor.isReadOnly ).to.be.true; - - hostnameStub.restore(); - } ); + sinon.assert.calledWithMatch( showErrorStub, 'invalid' ); + expect( editor.isReadOnly ).to.be.true; } ); } ); From 0cb15afa8b944bfb7569c898f4ac259123a50a2a Mon Sep 17 00:00:00 2001 From: Arkadiusz Filipczak Date: Thu, 9 May 2024 13:45:07 +0200 Subject: [PATCH 021/256] Domain check logic: misc. fixes after code review. --- packages/ckeditor5-core/src/editor/editor.ts | 4 ++-- packages/ckeditor5-core/tests/editor/editor.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/ckeditor5-core/src/editor/editor.ts b/packages/ckeditor5-core/src/editor/editor.ts index dbbd6c0f1d6..6abc5ae348f 100644 --- a/packages/ckeditor5-core/src/editor/editor.ts +++ b/packages/ckeditor5-core/src/editor/editor.ts @@ -704,7 +704,7 @@ export default abstract class Editor extends ObservableMixin() { return; } - const licensedHosts: Array = licensePayload.licensedHosts; + const licensedHosts: Array | undefined = licensePayload.licensedHosts; if ( licensedHosts ) { const hostname = window.location.hostname; @@ -716,7 +716,7 @@ export default abstract class Editor extends ObservableMixin() { const isWillcardMatched = willcards.some( willcard => willcard === hostname.slice( -willcard.length ) ); if ( !isWillcardMatched && !isHostnameMatched ) { - blockEditor( this, 'invalid' ); + blockEditor( this, 'domainLimit' ); return; } diff --git a/packages/ckeditor5-core/tests/editor/editor.js b/packages/ckeditor5-core/tests/editor/editor.js index 3749e2bea3c..c541ae76260 100644 --- a/packages/ckeditor5-core/tests/editor/editor.js +++ b/packages/ckeditor5-core/tests/editor/editor.js @@ -295,7 +295,7 @@ describe( 'Editor', () => { const editor = new TestEditor( { licenseKey } ); - sinon.assert.calledWithMatch( showErrorStub, 'invalid' ); + sinon.assert.calledWithMatch( showErrorStub, 'domainLimit' ); expect( editor.isReadOnly ).to.be.true; } ); @@ -314,7 +314,7 @@ describe( 'Editor', () => { const editor = new TestEditor( { licenseKey } ); - sinon.assert.calledWithMatch( showErrorStub, 'invalid' ); + sinon.assert.calledWithMatch( showErrorStub, 'domainLimit' ); expect( editor.isReadOnly ).to.be.true; } ); } ); From 2bce5a58718afa4a77d81e0af92c31631a92be25 Mon Sep 17 00:00:00 2001 From: Illia Sheremetov Date: Thu, 9 May 2024 16:02:09 +0200 Subject: [PATCH 022/256] Implement trial check. --- packages/ckeditor5-core/src/editor/editor.ts | 10 +++ .../ckeditor5-core/tests/editor/editor.js | 90 +++++++++++++++++++ 2 files changed, 100 insertions(+) diff --git a/packages/ckeditor5-core/src/editor/editor.ts b/packages/ckeditor5-core/src/editor/editor.ts index 6abc5ae348f..3cefa5926ba 100644 --- a/packages/ckeditor5-core/src/editor/editor.ts +++ b/packages/ckeditor5-core/src/editor/editor.ts @@ -722,6 +722,16 @@ export default abstract class Editor extends ObservableMixin() { } } + if ( licensePayload.licenseType === 'trial' && licensePayload.exp * 1000 < Date.now() ) { + blockEditor( this, 'trialLimit' ); + + return; + } + + if ( licensePayload.licenseType === 'trial' ) { + setTimeout( () => blockEditor( this, 'trialLimit' ), 600000 ); + } + if ( licensePayload.usageEndpoint ) { this.once( 'ready', () => { const telemetryData = this._getTelemetryData(); diff --git a/packages/ckeditor5-core/tests/editor/editor.js b/packages/ckeditor5-core/tests/editor/editor.js index c541ae76260..ec6350d67d4 100644 --- a/packages/ckeditor5-core/tests/editor/editor.js +++ b/packages/ckeditor5-core/tests/editor/editor.js @@ -319,6 +319,96 @@ describe( 'Editor', () => { } ); } ); + describe( 'trial check', () => { + beforeEach( () => { + sinon.useFakeTimers( { now: Date.now() } ); + } ); + + afterEach( () => { + sinon.restore(); + } ); + + it( 'should block if trial is expired', () => { + const licenseKey = 'foo.eyJleHAiOjE3MTUyMTI4MDAsImp0aSI6ImJkM2ZjNTc0LTJkNGYtNGNkZ' + + 'S1iNWViLTIzYzk1Y2JlMjQzYSIsImxpY2Vuc2VUeXBlIjoidHJpYWwiLCJ2YyI6ImZlOTdmNzY5In0.bar'; + + /** + * after decoding licenseKey: + * + * licensePaylod: { + * ..., + * exp: timestamp( 09.05.2024 ) + * licenseType: 'trial' + * } + */ + + const today = 1715166436000; // 08.05.2024 + const dateNow = sinon.stub( Date, 'now' ).returns( today ); + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.notCalled( showErrorStub ); + expect( editor.isReadOnly ).to.be.false; + + dateNow.restore(); + } ); + + it( 'should not block if trial is not expired', () => { + const licenseKey = 'foo.eyJleHAiOjE3MTUyMTI4MDAsImp0aSI6ImJkM2ZjNTc0LTJkNGYtNGNkZ' + + 'S1iNWViLTIzYzk1Y2JlMjQzYSIsImxpY2Vuc2VUeXBlIjoidHJpYWwiLCJ2YyI6ImZlOTdmNzY5In0.bar'; + + /** + * after decoding licenseKey: + * + * licensePaylod: { + * ..., + * exp: timestamp( 09.05.2024 ) + * licenseType: 'trial' + * } + */ + + const today = 1715339236000; // 10.05.2024 + const dateNow = sinon.stub( Date, 'now' ).returns( today ); + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.calledWithMatch( showErrorStub, 'trialLimit' ); + expect( editor.isReadOnly ).to.be.true; + + dateNow.restore(); + } ); + + it( 'should block editor after 10 minutes if trial license.', () => { + const licenseKey = 'foo.eyJleHAiOjE3MTUyMTI4MDAsImp0aSI6ImJkM2ZjNTc0LTJkNGYtNGNkZ' + + 'S1iNWViLTIzYzk1Y2JlMjQzYSIsImxpY2Vuc2VUeXBlIjoidHJpYWwiLCJ2YyI6ImZlOTdmNzY5In0.bar'; + + /** + * after decoding licenseKey: + * + * licensePaylod: { + * ..., + * exp: timestamp( 09.05.2024 ) + * licenseType: 'trial' + * } + */ + + const today = 1715166436000; // 08.05.2024 + const dateNow = sinon.stub( Date, 'now' ).returns( today ); + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.notCalled( showErrorStub ); + expect( editor.isReadOnly ).to.be.false; + + sinon.clock.tick( 600100 ); + + sinon.assert.calledWithMatch( showErrorStub, 'trialLimit' ); + expect( editor.isReadOnly ).to.be.true; + + dateNow.restore(); + } ); + } ); + it( 'should block the editor when the license key is not valid (expiration date in the past)', () => { const licenseKey = 'foo.eyJleHAiOjE3MDQwNjcyMDAsImp0aSI6ImZvbyIsInZjIjoiOTc4NTlGQkIifQo.bar'; From dfce49e6cb6f32e206e920f0695c26d917f5e6c2 Mon Sep 17 00:00:00 2001 From: Illia Sheremetov Date: Fri, 10 May 2024 13:30:14 +0200 Subject: [PATCH 023/256] Clear timer on editor destroy. --- packages/ckeditor5-core/src/editor/editor.ts | 21 ++++++++-- .../ckeditor5-core/tests/editor/editor.js | 38 +++++++++++++++++++ 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/packages/ckeditor5-core/src/editor/editor.ts b/packages/ckeditor5-core/src/editor/editor.ts index 3cefa5926ba..8a90148fb05 100644 --- a/packages/ckeditor5-core/src/editor/editor.ts +++ b/packages/ckeditor5-core/src/editor/editor.ts @@ -573,7 +573,9 @@ export default abstract class Editor extends ObservableMixin() { let readyPromise: Promise = Promise.resolve(); if ( this.state == 'initializing' ) { - readyPromise = new Promise( resolve => this.once( 'ready', resolve ) ); + readyPromise = new Promise( resolve => this.once( 'ready', val => { + resolve( val ); + } ) ); } return readyPromise @@ -723,13 +725,17 @@ export default abstract class Editor extends ObservableMixin() { } if ( licensePayload.licenseType === 'trial' && licensePayload.exp * 1000 < Date.now() ) { - blockEditor( this, 'trialLimit' ); + blockTrialEditor( this ); return; } if ( licensePayload.licenseType === 'trial' ) { - setTimeout( () => blockEditor( this, 'trialLimit' ), 600000 ); + const timerId = setTimeout( () => blockTrialEditor( this ), 600000 ); + + this.on( 'destroy', () => { + clearTimeout( timerId ); + } ); } if ( licensePayload.usageEndpoint ) { @@ -750,6 +756,15 @@ export default abstract class Editor extends ObservableMixin() { }, { priority: 'high' } ); } + function blockTrialEditor( editor: Editor ) { + blockEditor( editor, 'trialLimit' ); + + console.info( + 'You are using the trial version of CKEditor 5 plugin with limited usage. ' + + 'Make sure you will not use it in the production environment.' + ); + } + function getPayload( licenseKey: string ): string | null { const parts = licenseKey.split( '.' ); diff --git a/packages/ckeditor5-core/tests/editor/editor.js b/packages/ckeditor5-core/tests/editor/editor.js index ec6350d67d4..3fd70f5df5b 100644 --- a/packages/ckeditor5-core/tests/editor/editor.js +++ b/packages/ckeditor5-core/tests/editor/editor.js @@ -320,8 +320,11 @@ describe( 'Editor', () => { } ); describe( 'trial check', () => { + let consoleInfoSpy; + beforeEach( () => { sinon.useFakeTimers( { now: Date.now() } ); + consoleInfoSpy = sinon.spy( console, 'info' ); } ); afterEach( () => { @@ -374,6 +377,9 @@ describe( 'Editor', () => { sinon.assert.calledWithMatch( showErrorStub, 'trialLimit' ); expect( editor.isReadOnly ).to.be.true; + sinon.assert.calledOnce( consoleInfoSpy ); + sinon.assert.calledWith( consoleInfoSpy, 'You are using the trial version of CKEditor 5 plugin with ' + + 'limited usage. Make sure you will not use it in the production environment.' ); dateNow.restore(); } ); @@ -404,7 +410,39 @@ describe( 'Editor', () => { sinon.assert.calledWithMatch( showErrorStub, 'trialLimit' ); expect( editor.isReadOnly ).to.be.true; + sinon.assert.calledOnce( consoleInfoSpy ); + sinon.assert.calledWith( consoleInfoSpy, 'You are using the trial version of CKEditor 5 plugin with ' + + 'limited usage. Make sure you will not use it in the production environment.' ); + + dateNow.restore(); + } ); + + it( 'should clear timer on editor destroy', done => { + const licenseKey = 'foo.eyJleHAiOjE3MTUyMTI4MDAsImp0aSI6ImJkM2ZjNTc0LTJkNGYtNGNkZ' + + 'S1iNWViLTIzYzk1Y2JlMjQzYSIsImxpY2Vuc2VUeXBlIjoidHJpYWwiLCJ2YyI6ImZlOTdmNzY5In0.bar'; + + /** + * after decoding licenseKey: + * + * licensePaylod: { + * ..., + * exp: timestamp( 09.05.2024 ) + * licenseType: 'trial' + * } + */ + + const today = 1715166436000; // 08.05.2024 + const dateNow = sinon.stub( Date, 'now' ).returns( today ); + const editor = new TestEditor( { licenseKey } ); + const clearTimeoutSpy = sinon.spy( globalThis, 'clearTimeout' ); + + editor.fire( 'ready' ); + editor.on( 'destroy', () => { + sinon.assert.calledOnce( clearTimeoutSpy ); + done(); + } ); + editor.destroy(); dateNow.restore(); } ); } ); From a71cddcb7b3c27bdb2722f9e456f213061e8882f Mon Sep 17 00:00:00 2001 From: Illia Sheremetov Date: Fri, 10 May 2024 14:21:31 +0200 Subject: [PATCH 024/256] Remove reduntant changes. --- packages/ckeditor5-core/src/editor/editor.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/ckeditor5-core/src/editor/editor.ts b/packages/ckeditor5-core/src/editor/editor.ts index 8a90148fb05..8f7b34c66b2 100644 --- a/packages/ckeditor5-core/src/editor/editor.ts +++ b/packages/ckeditor5-core/src/editor/editor.ts @@ -573,9 +573,7 @@ export default abstract class Editor extends ObservableMixin() { let readyPromise: Promise = Promise.resolve(); if ( this.state == 'initializing' ) { - readyPromise = new Promise( resolve => this.once( 'ready', val => { - resolve( val ); - } ) ); + readyPromise = new Promise( resolve => this.once( 'ready', resolve ) ); } return readyPromise From a0e03826e8966aa09d4b95d38b58c07ce5d50efd Mon Sep 17 00:00:00 2001 From: Illia Sheremetov Date: Fri, 10 May 2024 14:57:47 +0200 Subject: [PATCH 025/256] Implement development check. --- packages/ckeditor5-core/src/editor/editor.ts | 23 ++++ .../ckeditor5-core/tests/editor/editor.js | 128 ++++++++++++++++++ 2 files changed, 151 insertions(+) diff --git a/packages/ckeditor5-core/src/editor/editor.ts b/packages/ckeditor5-core/src/editor/editor.ts index 6abc5ae348f..31567acc40e 100644 --- a/packages/ckeditor5-core/src/editor/editor.ts +++ b/packages/ckeditor5-core/src/editor/editor.ts @@ -722,6 +722,20 @@ export default abstract class Editor extends ObservableMixin() { } } + if ( licensePayload.licenseType === 'development' && licensePayload.exp * 1000 < Date.now() ) { + blockDevelopmentEditor( this ); + + return; + } + + if ( licensePayload.licenseType === 'development' ) { + const timerId = setTimeout( () => blockDevelopmentEditor( this ), 600000 ); + + this.on( 'destroy', () => { + clearTimeout( timerId ); + } ); + } + if ( licensePayload.usageEndpoint ) { this.once( 'ready', () => { const telemetryData = this._getTelemetryData(); @@ -740,6 +754,15 @@ export default abstract class Editor extends ObservableMixin() { }, { priority: 'high' } ); } + function blockDevelopmentEditor( editor: Editor ) { + blockEditor( editor, 'developmentLimit' ); + + console.info( + 'You are using the development version of CKEditor 5 plugin with limited usage. ' + + 'Make sure you will not use it in the production environment.' + ); + } + function getPayload( licenseKey: string ): string | null { const parts = licenseKey.split( '.' ); diff --git a/packages/ckeditor5-core/tests/editor/editor.js b/packages/ckeditor5-core/tests/editor/editor.js index c541ae76260..6591589a03c 100644 --- a/packages/ckeditor5-core/tests/editor/editor.js +++ b/packages/ckeditor5-core/tests/editor/editor.js @@ -319,6 +319,134 @@ describe( 'Editor', () => { } ); } ); + describe( 'development check', () => { + let consoleInfoSpy; + + beforeEach( () => { + sinon.useFakeTimers( { now: Date.now() } ); + consoleInfoSpy = sinon.spy( console, 'info' ); + } ); + + afterEach( () => { + sinon.restore(); + } ); + + it( 'should not block if development is not expired', () => { + const licenseKey = 'foo.eyJleHAiOjE3MTUyMTI4MDAsImp0aSI6IjczNDk5YTQyLWJjNzktNDdlNy1hNmR' + + 'lLWIyMGJhMmEzYmI4OSIsImxpY2Vuc2VUeXBlIjoiZGV2ZWxvcG1lbnQiLCJ2YyI6Ijg5NzRiYTJlIn0.bar'; + + /** + * after decoding licenseKey: + * + * licensePaylod: { + * ..., + * exp: timestamp( 09.05.2024 ) + * licenseType: 'development' + * } + */ + + const today = 1715166436000; // 08.05.2024 + const dateNow = sinon.stub( Date, 'now' ).returns( today ); + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.notCalled( showErrorStub ); + expect( editor.isReadOnly ).to.be.false; + + dateNow.restore(); + } ); + + it( 'should block if development is expired', () => { + const licenseKey = 'foo.eyJleHAiOjE3MTUyMTI4MDAsImp0aSI6IjczNDk5YTQyLWJjNzktNDdlNy1hNmR' + + 'lLWIyMGJhMmEzYmI4OSIsImxpY2Vuc2VUeXBlIjoiZGV2ZWxvcG1lbnQiLCJ2YyI6Ijg5NzRiYTJlIn0.bar'; + + /** + * after decoding licenseKey: + * + * licensePaylod: { + * ..., + * exp: timestamp( 09.05.2024 ) + * licenseType: 'development' + * } + */ + + const today = 1715339236000; // 10.05.2024 + const dateNow = sinon.stub( Date, 'now' ).returns( today ); + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.calledWithMatch( showErrorStub, 'developmentLimit' ); + expect( editor.isReadOnly ).to.be.true; + sinon.assert.calledOnce( consoleInfoSpy ); + sinon.assert.calledWith( consoleInfoSpy, 'You are using the development version of CKEditor 5 plugin with ' + + 'limited usage. Make sure you will not use it in the production environment.' ); + + dateNow.restore(); + } ); + + it( 'should block editor after 10 minutes if development license.', () => { + const licenseKey = 'foo.eyJleHAiOjE3MTUyMTI4MDAsImp0aSI6IjczNDk5YTQyLWJjNzktNDdlNy1hNmR' + + 'lLWIyMGJhMmEzYmI4OSIsImxpY2Vuc2VUeXBlIjoiZGV2ZWxvcG1lbnQiLCJ2YyI6Ijg5NzRiYTJlIn0.bar'; + + /** + * after decoding licenseKey: + * + * licensePaylod: { + * ..., + * exp: timestamp( 09.05.2024 ) + * licenseType: 'development' + * } + */ + + const today = 1715166436000; // 08.05.2024 + const dateNow = sinon.stub( Date, 'now' ).returns( today ); + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.notCalled( showErrorStub ); + expect( editor.isReadOnly ).to.be.false; + + sinon.clock.tick( 600100 ); + + sinon.assert.calledWithMatch( showErrorStub, 'developmentLimit' ); + expect( editor.isReadOnly ).to.be.true; + sinon.assert.calledOnce( consoleInfoSpy ); + sinon.assert.calledWith( consoleInfoSpy, 'You are using the development version of CKEditor 5 plugin with ' + + 'limited usage. Make sure you will not use it in the production environment.' ); + + dateNow.restore(); + } ); + + it( 'should clear timer on editor destroy', done => { + const licenseKey = 'foo.eyJleHAiOjE3MTUyMTI4MDAsImp0aSI6IjczNDk5YTQyLWJjNzktNDdlNy1hNmR' + + 'lLWIyMGJhMmEzYmI4OSIsImxpY2Vuc2VUeXBlIjoiZGV2ZWxvcG1lbnQiLCJ2YyI6Ijg5NzRiYTJlIn0.bar'; + + /** + * after decoding licenseKey: + * + * licensePaylod: { + * ..., + * exp: timestamp( 09.05.2024 ) + * licenseType: 'development' + * } + */ + + const today = 1715166436000; // 08.05.2024 + const dateNow = sinon.stub( Date, 'now' ).returns( today ); + const editor = new TestEditor( { licenseKey } ); + const clearTimeoutSpy = sinon.spy( globalThis, 'clearTimeout' ); + + editor.fire( 'ready' ); + editor.on( 'destroy', () => { + sinon.assert.calledOnce( clearTimeoutSpy ); + done(); + } ); + + editor.destroy(); + dateNow.restore(); + } ); + } ); + it( 'should block the editor when the license key is not valid (expiration date in the past)', () => { const licenseKey = 'foo.eyJleHAiOjE3MDQwNjcyMDAsImp0aSI6ImZvbyIsInZjIjoiOTc4NTlGQkIifQo.bar'; From 04cc58c7a3de22a2e7b424b8da69c298a5f30688 Mon Sep 17 00:00:00 2001 From: Illia Sheremetov Date: Fri, 10 May 2024 15:15:19 +0200 Subject: [PATCH 026/256] Fix test name. --- packages/ckeditor5-core/tests/editor/editor.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ckeditor5-core/tests/editor/editor.js b/packages/ckeditor5-core/tests/editor/editor.js index 3fd70f5df5b..6fe1a42fad4 100644 --- a/packages/ckeditor5-core/tests/editor/editor.js +++ b/packages/ckeditor5-core/tests/editor/editor.js @@ -331,7 +331,7 @@ describe( 'Editor', () => { sinon.restore(); } ); - it( 'should block if trial is expired', () => { + it( 'should not block if trial is not expired', () => { const licenseKey = 'foo.eyJleHAiOjE3MTUyMTI4MDAsImp0aSI6ImJkM2ZjNTc0LTJkNGYtNGNkZ' + 'S1iNWViLTIzYzk1Y2JlMjQzYSIsImxpY2Vuc2VUeXBlIjoidHJpYWwiLCJ2YyI6ImZlOTdmNzY5In0.bar'; @@ -356,7 +356,7 @@ describe( 'Editor', () => { dateNow.restore(); } ); - it( 'should not block if trial is not expired', () => { + it( 'should block if trial is expired', () => { const licenseKey = 'foo.eyJleHAiOjE3MTUyMTI4MDAsImp0aSI6ImJkM2ZjNTc0LTJkNGYtNGNkZ' + 'S1iNWViLTIzYzk1Y2JlMjQzYSIsImxpY2Vuc2VUeXBlIjoidHJpYWwiLCJ2YyI6ImZlOTdmNzY5In0.bar'; From 7bfe1efe0234d7e552df5042cf390a1916020972 Mon Sep 17 00:00:00 2001 From: Illia Sheremetov Date: Mon, 13 May 2024 14:07:48 +0200 Subject: [PATCH 027/256] Fix console warn. --- packages/ckeditor5-core/src/editor/editor.ts | 2 +- packages/ckeditor5-core/tests/editor/editor.js | 4 ++-- packages/ckeditor5-font/tests/manual/font-color.js | 4 +++- packages/ckeditor5-ui/src/editorui/poweredby.ts | 11 +++++++++-- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/ckeditor5-core/src/editor/editor.ts b/packages/ckeditor5-core/src/editor/editor.ts index 31567acc40e..0658140bcbe 100644 --- a/packages/ckeditor5-core/src/editor/editor.ts +++ b/packages/ckeditor5-core/src/editor/editor.ts @@ -758,7 +758,7 @@ export default abstract class Editor extends ObservableMixin() { blockEditor( editor, 'developmentLimit' ); console.info( - 'You are using the development version of CKEditor 5 plugin with limited usage. ' + + 'You are using the development version of CKEditor 5 with limited usage. ' + 'Make sure you will not use it in the production environment.' ); } diff --git a/packages/ckeditor5-core/tests/editor/editor.js b/packages/ckeditor5-core/tests/editor/editor.js index 6591589a03c..200d7cd6887 100644 --- a/packages/ckeditor5-core/tests/editor/editor.js +++ b/packages/ckeditor5-core/tests/editor/editor.js @@ -378,7 +378,7 @@ describe( 'Editor', () => { sinon.assert.calledWithMatch( showErrorStub, 'developmentLimit' ); expect( editor.isReadOnly ).to.be.true; sinon.assert.calledOnce( consoleInfoSpy ); - sinon.assert.calledWith( consoleInfoSpy, 'You are using the development version of CKEditor 5 plugin with ' + + sinon.assert.calledWith( consoleInfoSpy, 'You are using the development version of CKEditor 5 with ' + 'limited usage. Make sure you will not use it in the production environment.' ); dateNow.restore(); @@ -411,7 +411,7 @@ describe( 'Editor', () => { sinon.assert.calledWithMatch( showErrorStub, 'developmentLimit' ); expect( editor.isReadOnly ).to.be.true; sinon.assert.calledOnce( consoleInfoSpy ); - sinon.assert.calledWith( consoleInfoSpy, 'You are using the development version of CKEditor 5 plugin with ' + + sinon.assert.calledWith( consoleInfoSpy, 'You are using the development version of CKEditor 5 with ' + 'limited usage. Make sure you will not use it in the production environment.' ); dateNow.restore(); diff --git a/packages/ckeditor5-font/tests/manual/font-color.js b/packages/ckeditor5-font/tests/manual/font-color.js index b5823f5a928..d93487bba72 100644 --- a/packages/ckeditor5-font/tests/manual/font-color.js +++ b/packages/ckeditor5-font/tests/manual/font-color.js @@ -34,7 +34,9 @@ ClassicEditor ], fontColor: { columns: 3 - } + }, + licenseKey: 'foo.eyJleHAiOjE3MTY0MjI0MDAsImp0aSI6ImY5ZTViYjc5LTYzZTgtNGE0NS05YWQxLTg5YjBiNm' + + 'ZlNjE3MyIsImxpY2Vuc2VUeXBlIjoiZGV2ZWxvcG1lbnQiLCJ2YyI6ImU4NzdmNGE3In0.bar' } ) .then( editor => { window.editor = editor; diff --git a/packages/ckeditor5-ui/src/editorui/poweredby.ts b/packages/ckeditor5-ui/src/editorui/poweredby.ts index 7b858d2e0cd..623d20248cd 100644 --- a/packages/ckeditor5-ui/src/editorui/poweredby.ts +++ b/packages/ckeditor5-ui/src/editorui/poweredby.ts @@ -106,7 +106,7 @@ export default class PoweredBy extends DomEmitterMixin() { const licenseKey = editor.config.get( 'licenseKey' ); const licenseContent = licenseKey && parseBase64EncodedObject( licenseKey.split( '.' )[ 1 ] ); - if ( licenseContent && licenseContent.whiteLabel ) { + if ( licenseContent && ( licenseContent.whiteLabel || licenseContent.licenseType !== 'development' ) ) { return; } } @@ -357,10 +357,17 @@ function getLowerCornerPosition( function getNormalizedConfig( editor: Editor ): PoweredByConfig { const userConfig = editor.config.get( 'ui.poweredBy' ); const position = userConfig && userConfig.position || 'border'; + const licenseKey = editor.config.get( 'licenseKey' ); + const licenseContent = licenseKey && parseBase64EncodedObject( licenseKey.split( '.' )[ 1 ] ); + let label = DEFAULT_LABEL; + + if ( licenseContent && licenseContent.licenseType === 'development' ) { + label = 'Development'; + } return { position, - label: DEFAULT_LABEL, + label, verticalOffset: position === 'inside' ? 5 : 0, horizontalOffset: 5, From c0741ec0dde5d0a5d306dfc6f7253450abee28ff Mon Sep 17 00:00:00 2001 From: Arkadiusz Filipczak Date: Mon, 13 May 2024 14:11:09 +0200 Subject: [PATCH 028/256] Added internal to some editor methods. --- packages/ckeditor5-core/src/editor/editor.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/ckeditor5-core/src/editor/editor.ts b/packages/ckeditor5-core/src/editor/editor.ts index 6abc5ae348f..1119036fc60 100644 --- a/packages/ckeditor5-core/src/editor/editor.ts +++ b/packages/ckeditor5-core/src/editor/editor.ts @@ -659,6 +659,8 @@ export default abstract class Editor extends ObservableMixin() { /** * Performs basic license key check. Enables the editor's read-only mode if the license key's validation period has expired * or the license key format is incorrect. + * + * @internal */ private _verifyLicenseKey() { const licenseKey = this.config.get( 'licenseKey' ); @@ -790,6 +792,9 @@ export default abstract class Editor extends ObservableMixin() { }, 0 ); } + /** + * @internal + */ private async _sendUsageRequest( endpoint: string, licenseKey: string, From 852cc79010607ed9dd8676cc37b071b6f3e6414e Mon Sep 17 00:00:00 2001 From: Illia Sheremetov Date: Mon, 13 May 2024 15:21:29 +0200 Subject: [PATCH 029/256] Implement development license check. --- packages/ckeditor5-core/src/editor/editor.ts | 59 ++++---- .../ckeditor5-core/tests/editor/editor.js | 132 +++++++++++++++++- .../ckeditor5-ui/tests/editorui/poweredby.js | 22 +++ 3 files changed, 178 insertions(+), 35 deletions(-) diff --git a/packages/ckeditor5-core/src/editor/editor.ts b/packages/ckeditor5-core/src/editor/editor.ts index 77a526348ba..1b6ed63701b 100644 --- a/packages/ckeditor5-core/src/editor/editor.ts +++ b/packages/ckeditor5-core/src/editor/editor.ts @@ -724,34 +724,12 @@ export default abstract class Editor extends ObservableMixin() { } } - if ( licensePayload.licenseType === 'development' && licensePayload.exp * 1000 < Date.now() ) { - blockDevelopmentEditor( this ); + const licensedTypes: Array> = [ 'development', 'trial' ]; + if ( isLicenseTypeExpired( this, licensedTypes ) ) { return; } - if ( licensePayload.licenseType === 'development' ) { - const timerId = setTimeout( () => blockDevelopmentEditor( this ), 600000 ); - - this.on( 'destroy', () => { - clearTimeout( timerId ); - } ); - } - - if ( licensePayload.licenseType === 'trial' && licensePayload.exp * 1000 < Date.now() ) { - blockTrialEditor( this ); - - return; - } - - if ( licensePayload.licenseType === 'trial' ) { - const timerId = setTimeout( () => blockTrialEditor( this ), 600000 ); - - this.on( 'destroy', () => { - clearTimeout( timerId ); - } ); - } - if ( licensePayload.usageEndpoint ) { this.once( 'ready', () => { const telemetryData = this._getTelemetryData(); @@ -770,20 +748,33 @@ export default abstract class Editor extends ObservableMixin() { }, { priority: 'high' } ); } - function blockDevelopmentEditor( editor: Editor ) { - blockEditor( editor, 'developmentLimit' ); + function isLicenseTypeExpired( editor: Editor, licenseTypes: Array> ): boolean { + let isExpired = false; - console.info( - 'You are using the development version of CKEditor 5 with limited usage. ' + - 'Make sure you will not use it in the production environment.' - ); + licenseTypes.forEach( licenseType => { + if ( licensePayload && licensePayload.licenseType === licenseType && licensePayload.exp * 1000 < Date.now() ) { + blockEditorWithInfo( editor, licenseType ); + + isExpired = true; + } + + if ( licensePayload && licensePayload.licenseType === licenseType ) { + const timerId = setTimeout( () => blockEditorWithInfo( editor, licenseType ), 600000 /* 10 minutes */ ); + + editor.on( 'destroy', () => { + clearTimeout( timerId ); + } ); + } + } ); + + return isExpired; } - function blockTrialEditor( editor: Editor ) { - blockEditor( editor, 'trialLimit' ); + function blockEditorWithInfo( editor: Editor, licenseType: Exclude ) { + blockEditor( editor, `${ licenseType }Limit` ); console.info( - 'You are using the trial version of CKEditor 5 plugin with limited usage. ' + + `You are using the ${ licenseType } version of CKEditor 5 with limited usage. ` + 'Make sure you will not use it in the production environment.' ); } @@ -869,6 +860,8 @@ export default abstract class Editor extends ObservableMixin() { type LicenseErrorReason = 'invalid' | 'expired' | 'domainLimit' | 'featureNotAllowed' | 'trialLimit' | 'developmentLimit' | 'usageLimit'; +type LicenseType = 'trial' | 'development' | 'production'; + /** * Fired when the {@link module:engine/controller/datacontroller~DataController#event:ready data} and all additional * editor components are ready. diff --git a/packages/ckeditor5-core/tests/editor/editor.js b/packages/ckeditor5-core/tests/editor/editor.js index 6fe1a42fad4..24201a9e1ce 100644 --- a/packages/ckeditor5-core/tests/editor/editor.js +++ b/packages/ckeditor5-core/tests/editor/editor.js @@ -378,7 +378,7 @@ describe( 'Editor', () => { sinon.assert.calledWithMatch( showErrorStub, 'trialLimit' ); expect( editor.isReadOnly ).to.be.true; sinon.assert.calledOnce( consoleInfoSpy ); - sinon.assert.calledWith( consoleInfoSpy, 'You are using the trial version of CKEditor 5 plugin with ' + + sinon.assert.calledWith( consoleInfoSpy, 'You are using the trial version of CKEditor 5 with ' + 'limited usage. Make sure you will not use it in the production environment.' ); dateNow.restore(); @@ -411,7 +411,7 @@ describe( 'Editor', () => { sinon.assert.calledWithMatch( showErrorStub, 'trialLimit' ); expect( editor.isReadOnly ).to.be.true; sinon.assert.calledOnce( consoleInfoSpy ); - sinon.assert.calledWith( consoleInfoSpy, 'You are using the trial version of CKEditor 5 plugin with ' + + sinon.assert.calledWith( consoleInfoSpy, 'You are using the trial version of CKEditor 5 with ' + 'limited usage. Make sure you will not use it in the production environment.' ); dateNow.restore(); @@ -447,6 +447,134 @@ describe( 'Editor', () => { } ); } ); + describe( 'development check', () => { + let consoleInfoSpy; + + beforeEach( () => { + sinon.useFakeTimers( { now: Date.now() } ); + consoleInfoSpy = sinon.spy( console, 'info' ); + } ); + + afterEach( () => { + sinon.restore(); + } ); + + it( 'should not block if development is not expired', () => { + const licenseKey = 'foo.eyJleHAiOjE3MTUyMTI4MDAsImp0aSI6IjczNDk5YTQyLWJjNzktNDdlNy1hNmR' + + 'lLWIyMGJhMmEzYmI4OSIsImxpY2Vuc2VUeXBlIjoiZGV2ZWxvcG1lbnQiLCJ2YyI6Ijg5NzRiYTJlIn0.bar'; + + /** + * after decoding licenseKey: + * + * licensePaylod: { + * ..., + * exp: timestamp( 09.05.2024 ) + * licenseType: 'development' + * } + */ + + const today = 1715166436000; // 08.05.2024 + const dateNow = sinon.stub( Date, 'now' ).returns( today ); + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.notCalled( showErrorStub ); + expect( editor.isReadOnly ).to.be.false; + + dateNow.restore(); + } ); + + it( 'should block if development is expired', () => { + const licenseKey = 'foo.eyJleHAiOjE3MTUyMTI4MDAsImp0aSI6IjczNDk5YTQyLWJjNzktNDdlNy1hNmR' + + 'lLWIyMGJhMmEzYmI4OSIsImxpY2Vuc2VUeXBlIjoiZGV2ZWxvcG1lbnQiLCJ2YyI6Ijg5NzRiYTJlIn0.bar'; + + /** + * after decoding licenseKey: + * + * licensePaylod: { + * ..., + * exp: timestamp( 09.05.2024 ) + * licenseType: 'development' + * } + */ + + const today = 1715339236000; // 10.05.2024 + const dateNow = sinon.stub( Date, 'now' ).returns( today ); + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.calledWithMatch( showErrorStub, 'developmentLimit' ); + expect( editor.isReadOnly ).to.be.true; + sinon.assert.calledOnce( consoleInfoSpy ); + sinon.assert.calledWith( consoleInfoSpy, 'You are using the development version of CKEditor 5 with ' + + 'limited usage. Make sure you will not use it in the production environment.' ); + + dateNow.restore(); + } ); + + it( 'should block editor after 10 minutes if development license.', () => { + const licenseKey = 'foo.eyJleHAiOjE3MTUyMTI4MDAsImp0aSI6IjczNDk5YTQyLWJjNzktNDdlNy1hNmR' + + 'lLWIyMGJhMmEzYmI4OSIsImxpY2Vuc2VUeXBlIjoiZGV2ZWxvcG1lbnQiLCJ2YyI6Ijg5NzRiYTJlIn0.bar'; + + /** + * after decoding licenseKey: + * + * licensePaylod: { + * ..., + * exp: timestamp( 09.05.2024 ) + * licenseType: 'development' + * } + */ + + const today = 1715166436000; // 08.05.2024 + const dateNow = sinon.stub( Date, 'now' ).returns( today ); + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.notCalled( showErrorStub ); + expect( editor.isReadOnly ).to.be.false; + + sinon.clock.tick( 600100 ); + + sinon.assert.calledWithMatch( showErrorStub, 'developmentLimit' ); + expect( editor.isReadOnly ).to.be.true; + sinon.assert.calledOnce( consoleInfoSpy ); + sinon.assert.calledWith( consoleInfoSpy, 'You are using the development version of CKEditor 5 with ' + + 'limited usage. Make sure you will not use it in the production environment.' ); + + dateNow.restore(); + } ); + + it( 'should clear timer on editor destroy', done => { + const licenseKey = 'foo.eyJleHAiOjE3MTUyMTI4MDAsImp0aSI6IjczNDk5YTQyLWJjNzktNDdlNy1hNmR' + + 'lLWIyMGJhMmEzYmI4OSIsImxpY2Vuc2VUeXBlIjoiZGV2ZWxvcG1lbnQiLCJ2YyI6Ijg5NzRiYTJlIn0.bar'; + + /** + * after decoding licenseKey: + * + * licensePaylod: { + * ..., + * exp: timestamp( 09.05.2024 ) + * licenseType: 'development' + * } + */ + + const today = 1715166436000; // 08.05.2024 + const dateNow = sinon.stub( Date, 'now' ).returns( today ); + const editor = new TestEditor( { licenseKey } ); + const clearTimeoutSpy = sinon.spy( globalThis, 'clearTimeout' ); + + editor.fire( 'ready' ); + editor.on( 'destroy', () => { + sinon.assert.calledOnce( clearTimeoutSpy ); + done(); + } ); + + editor.destroy(); + dateNow.restore(); + } ); + } ); + it( 'should block the editor when the license key is not valid (expiration date in the past)', () => { const licenseKey = 'foo.eyJleHAiOjE3MDQwNjcyMDAsImp0aSI6ImZvbyIsInZjIjoiOTc4NTlGQkIifQo.bar'; diff --git a/packages/ckeditor5-ui/tests/editorui/poweredby.js b/packages/ckeditor5-ui/tests/editorui/poweredby.js index 56d5a60d5c4..53bee5bb05f 100644 --- a/packages/ckeditor5-ui/tests/editorui/poweredby.js +++ b/packages/ckeditor5-ui/tests/editorui/poweredby.js @@ -129,6 +129,28 @@ describe( 'PoweredBy', () => { await editor.destroy(); } ); + + it( 'should create the balloon when a development license key is configured', async () => { + const editor = await createEditor( element, { + licenseKey: 'foo.eyJleHAiOjE3MTUyMTI4MDAsImp0aSI6IjczNDk5YTQyLWJjNzktNDdlNy1hNmR' + + 'lLWIyMGJhMmEzYmI4OSIsImxpY2Vuc2VUeXBlIjoiZGV2ZWxvcG1lbnQiLCJ2YyI6Ijg5NzRiYTJlIn0.bar' + } ); + + expect( editor.ui.poweredBy._balloonView ).to.be.null; + + focusEditor( editor ); + + expect( editor.ui.poweredBy._balloonView ).to.be.instanceOf( BalloonPanelView ); + + const view = editor.ui.poweredBy._balloonView.content.first; + + expect( view.element.firstChild.firstChild.tagName ).to.equal( 'SPAN' ); + expect( view.element.firstChild.firstChild.classList.contains( 'ck' ) ).to.be.true; + expect( view.element.firstChild.firstChild.classList.contains( 'ck-powered-by__label' ) ).to.be.true; + expect( view.element.firstChild.firstChild.textContent ).to.equal( 'Development' ); + + await editor.destroy(); + } ); } ); describe( 'balloon management on editor focus change', () => { From d03f796eb5e5cf10acde3d15fbf87087af275ed8 Mon Sep 17 00:00:00 2001 From: Arkadiusz Filipczak Date: Wed, 15 May 2024 10:40:16 +0200 Subject: [PATCH 030/256] License v3 - usage check refactored. --- packages/ckeditor5-core/src/editor/editor.ts | 29 ++++++++++---------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/packages/ckeditor5-core/src/editor/editor.ts b/packages/ckeditor5-core/src/editor/editor.ts index 5f7446c4141..c046849950a 100644 --- a/packages/ckeditor5-core/src/editor/editor.ts +++ b/packages/ckeditor5-core/src/editor/editor.ts @@ -740,9 +740,16 @@ export default abstract class Editor extends ObservableMixin() { if ( licensePayload.usageEndpoint ) { this.once( 'ready', () => { - const telemetryData = this._getTelemetryData(); + const telemetry = this._getTelemetryData(); - this._sendUsageRequest( licensePayload.usageEndpoint, licenseKey, telemetryData ).then( response => { + const request = { + requestId: uid(), + requestTime: Math.round( Date.now() / 1000 ), + license: licenseKey, + telemetry + }; + + this._sendUsageRequest( licensePayload.usageEndpoint, request ).then( response => { const { status, message } = response; if ( message ) { @@ -801,6 +808,9 @@ export default abstract class Editor extends ObservableMixin() { } } + /** + * @internal + */ /* istanbul ignore next -- @preserve */ // eslint-disable-next-line @typescript-eslint/no-unused-vars private _showLicenseError( reason: LicenseErrorReason, featureName?: string ) { @@ -813,23 +823,14 @@ export default abstract class Editor extends ObservableMixin() { */ throw new CKEditorError( 'todo-specify-this-error-code', null ); }, 0 ); + + this._showLicenseError = () => {}; } /** * @internal */ - private async _sendUsageRequest( - endpoint: string, - licenseKey: string, - telemetry: Record - ) { - const request = { - requestId: uid(), - requestTime: Math.round( Date.now() / 1000 ), - license: licenseKey, - telemetry - }; - + private async _sendUsageRequest( endpoint: string, request: unknown ) { const response = await fetch( new URL( endpoint ), { method: 'POST', body: JSON.stringify( request ) From 8acd285c21827f771c30a47b91d1ba56edd51559 Mon Sep 17 00:00:00 2001 From: Illia Sheremetov Date: Wed, 15 May 2024 14:07:12 +0200 Subject: [PATCH 031/256] Code review fixes. --- packages/ckeditor5-core/src/editor/editor.ts | 59 ++++++++----------- .../ckeditor5-core/tests/editor/editor.js | 53 ----------------- 2 files changed, 23 insertions(+), 89 deletions(-) diff --git a/packages/ckeditor5-core/src/editor/editor.ts b/packages/ckeditor5-core/src/editor/editor.ts index 1b6ed63701b..1b011655910 100644 --- a/packages/ckeditor5-core/src/editor/editor.ts +++ b/packages/ckeditor5-core/src/editor/editor.ts @@ -724,10 +724,30 @@ export default abstract class Editor extends ObservableMixin() { } } - const licensedTypes: Array> = [ 'development', 'trial' ]; + if ( licensePayload.licenseType === 'trial' && licensePayload.exp * 1000 < Date.now() ) { + blockEditor( this, 'trialLimit' ); - if ( isLicenseTypeExpired( this, licensedTypes ) ) { - return; + console.info( + 'You are using the trial version of CKEditor 5 with limited usage. ' + + 'Make sure you will not use it in the production environment.' + ); + } + + if ( licensePayload.licenseType === 'trial' || licensePayload.licenseType === 'development' ) { + const licenseType: 'trial' | 'development' = licensePayload.licenseType; + + const timerId = setTimeout( () => { + blockEditor( this, `${ licenseType }Limit` ); + + console.info( + `You are using the ${ licenseType } version of CKEditor 5 with limited usage. ` + + 'Make sure you will not use it in the production environment.' + ); + }, 600000 /* 10 minutes */ ); + + this.on( 'destroy', () => { + clearTimeout( timerId ); + } ); } if ( licensePayload.usageEndpoint ) { @@ -748,37 +768,6 @@ export default abstract class Editor extends ObservableMixin() { }, { priority: 'high' } ); } - function isLicenseTypeExpired( editor: Editor, licenseTypes: Array> ): boolean { - let isExpired = false; - - licenseTypes.forEach( licenseType => { - if ( licensePayload && licensePayload.licenseType === licenseType && licensePayload.exp * 1000 < Date.now() ) { - blockEditorWithInfo( editor, licenseType ); - - isExpired = true; - } - - if ( licensePayload && licensePayload.licenseType === licenseType ) { - const timerId = setTimeout( () => blockEditorWithInfo( editor, licenseType ), 600000 /* 10 minutes */ ); - - editor.on( 'destroy', () => { - clearTimeout( timerId ); - } ); - } - } ); - - return isExpired; - } - - function blockEditorWithInfo( editor: Editor, licenseType: Exclude ) { - blockEditor( editor, `${ licenseType }Limit` ); - - console.info( - `You are using the ${ licenseType } version of CKEditor 5 with limited usage. ` + - 'Make sure you will not use it in the production environment.' - ); - } - function getPayload( licenseKey: string ): string | null { const parts = licenseKey.split( '.' ); @@ -860,8 +849,6 @@ export default abstract class Editor extends ObservableMixin() { type LicenseErrorReason = 'invalid' | 'expired' | 'domainLimit' | 'featureNotAllowed' | 'trialLimit' | 'developmentLimit' | 'usageLimit'; -type LicenseType = 'trial' | 'development' | 'production'; - /** * Fired when the {@link module:engine/controller/datacontroller~DataController#event:ready data} and all additional * editor components are ready. diff --git a/packages/ckeditor5-core/tests/editor/editor.js b/packages/ckeditor5-core/tests/editor/editor.js index 24201a9e1ce..543e654f89e 100644 --- a/packages/ckeditor5-core/tests/editor/editor.js +++ b/packages/ckeditor5-core/tests/editor/editor.js @@ -459,59 +459,6 @@ describe( 'Editor', () => { sinon.restore(); } ); - it( 'should not block if development is not expired', () => { - const licenseKey = 'foo.eyJleHAiOjE3MTUyMTI4MDAsImp0aSI6IjczNDk5YTQyLWJjNzktNDdlNy1hNmR' + - 'lLWIyMGJhMmEzYmI4OSIsImxpY2Vuc2VUeXBlIjoiZGV2ZWxvcG1lbnQiLCJ2YyI6Ijg5NzRiYTJlIn0.bar'; - - /** - * after decoding licenseKey: - * - * licensePaylod: { - * ..., - * exp: timestamp( 09.05.2024 ) - * licenseType: 'development' - * } - */ - - const today = 1715166436000; // 08.05.2024 - const dateNow = sinon.stub( Date, 'now' ).returns( today ); - - const editor = new TestEditor( { licenseKey } ); - - sinon.assert.notCalled( showErrorStub ); - expect( editor.isReadOnly ).to.be.false; - - dateNow.restore(); - } ); - - it( 'should block if development is expired', () => { - const licenseKey = 'foo.eyJleHAiOjE3MTUyMTI4MDAsImp0aSI6IjczNDk5YTQyLWJjNzktNDdlNy1hNmR' + - 'lLWIyMGJhMmEzYmI4OSIsImxpY2Vuc2VUeXBlIjoiZGV2ZWxvcG1lbnQiLCJ2YyI6Ijg5NzRiYTJlIn0.bar'; - - /** - * after decoding licenseKey: - * - * licensePaylod: { - * ..., - * exp: timestamp( 09.05.2024 ) - * licenseType: 'development' - * } - */ - - const today = 1715339236000; // 10.05.2024 - const dateNow = sinon.stub( Date, 'now' ).returns( today ); - - const editor = new TestEditor( { licenseKey } ); - - sinon.assert.calledWithMatch( showErrorStub, 'developmentLimit' ); - expect( editor.isReadOnly ).to.be.true; - sinon.assert.calledOnce( consoleInfoSpy ); - sinon.assert.calledWith( consoleInfoSpy, 'You are using the development version of CKEditor 5 with ' + - 'limited usage. Make sure you will not use it in the production environment.' ); - - dateNow.restore(); - } ); - it( 'should block editor after 10 minutes if development license.', () => { const licenseKey = 'foo.eyJleHAiOjE3MTUyMTI4MDAsImp0aSI6IjczNDk5YTQyLWJjNzktNDdlNy1hNmR' + 'lLWIyMGJhMmEzYmI4OSIsImxpY2Vuc2VUeXBlIjoiZGV2ZWxvcG1lbnQiLCJ2YyI6Ijg5NzRiYTJlIn0.bar'; From 62ff9d17fa5122e14b968dd100e4998a8bf90a00 Mon Sep 17 00:00:00 2001 From: Illia Sheremetov Date: Wed, 15 May 2024 14:10:08 +0200 Subject: [PATCH 032/256] Add 'return' in license check. --- packages/ckeditor5-core/src/editor/editor.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/ckeditor5-core/src/editor/editor.ts b/packages/ckeditor5-core/src/editor/editor.ts index 1b011655910..9ec70c91134 100644 --- a/packages/ckeditor5-core/src/editor/editor.ts +++ b/packages/ckeditor5-core/src/editor/editor.ts @@ -731,6 +731,8 @@ export default abstract class Editor extends ObservableMixin() { 'You are using the trial version of CKEditor 5 with limited usage. ' + 'Make sure you will not use it in the production environment.' ); + + return; } if ( licensePayload.licenseType === 'trial' || licensePayload.licenseType === 'development' ) { From ea652b42733904e1ace220843951b0be09c34f99 Mon Sep 17 00:00:00 2001 From: Illia Sheremetov Date: Thu, 16 May 2024 18:05:03 +0200 Subject: [PATCH 033/256] Refactor license tests. --- .../ckeditor5-core/tests/editor/editor.js | 387 +-------------- .../tests/editor/licensecheck.js | 441 ++++++++++++++++++ 2 files changed, 442 insertions(+), 386 deletions(-) create mode 100644 packages/ckeditor5-core/tests/editor/licensecheck.js diff --git a/packages/ckeditor5-core/tests/editor/editor.js b/packages/ckeditor5-core/tests/editor/editor.js index 6fe1a42fad4..4882f449c00 100644 --- a/packages/ckeditor5-core/tests/editor/editor.js +++ b/packages/ckeditor5-core/tests/editor/editor.js @@ -3,7 +3,7 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ -/* globals document, window, setTimeout, console, Response, globalThis */ +/* globals document, window, setTimeout */ import Editor from '../../src/editor/editor.js'; import Context from '../../src/context.js'; @@ -215,385 +215,6 @@ describe( 'Editor', () => { sinon.assert.calledWith( spy, editor.editing.view.document ); } ); - - describe( 'license key verification', () => { - let showErrorStub; - - beforeEach( () => { - showErrorStub = testUtils.sinon.stub( TestEditor.prototype, '_showLicenseError' ); - } ); - - describe( 'required fields in the license key', () => { - it( 'should not block the editor when required fields are provided and are valid', () => { - const licenseKey = 'foo.eyJleHAiOjIyMDg5ODg4MDAsImp0aSI6ImZvbyIsInZjIjoiNDYyYTkzMGQifQ.bar'; - - const editor = new TestEditor( { licenseKey } ); - - sinon.assert.notCalled( showErrorStub ); - expect( editor.isReadOnly ).to.be.false; - } ); - - it( 'should block the editor when the `exp` field is missing', () => { - const licenseKey = 'foo.eyJqdGkiOiJmb28iLCJ2YyI6IjhjNzM2NTIxIn0.bar'; - const editor = new TestEditor( { licenseKey } ); - - sinon.assert.calledWithMatch( showErrorStub, 'invalid' ); - expect( editor.isReadOnly ).to.be.true; - } ); - - it( 'should block the editor when the `jti` field is missing', () => { - const licenseKey = 'foo.eyJleHAiOjIyMDg5ODg4MDAsInZjIjoiYzU1NmFkNzQifQ.bar'; - - const editor = new TestEditor( { licenseKey } ); - - sinon.assert.calledWithMatch( showErrorStub, 'invalid' ); - expect( editor.isReadOnly ).to.be.true; - } ); - - it( 'should block the editor when the `vc` field is missing', () => { - const licenseKey = 'foo.eyJleHAiOjIyMDg5ODg4MDAsImp0aSI6ImZvbyJ9Cg.bar'; - - const editor = new TestEditor( { licenseKey } ); - - sinon.assert.calledWithMatch( showErrorStub, 'invalid' ); - expect( editor.isReadOnly ).to.be.true; - } ); - } ); - - describe( 'domain check', () => { - it( 'should pass when localhost is in the licensedHosts list', () => { - const licenseKey = 'foo.eyJleHAiOjE3MTc1NDU2MDAsImp0aSI6Ijg0YWY4MjU4LTkxOTUtN' + - 'DllMy1iYzRhLTkwMWIzOTJmNGQ4ZiIsImxpY2Vuc2VkSG9zdHMiOlsibG9jYWxob3N0Il0sInZjIjoiYjY0ZjAwYmQifQ.bar'; - - /** - * after decoding licenseKey: - * - * licensePaylod: { - * ..., - * licensedHosts: [ 'localhost' ] - * } - */ - - const editor = new TestEditor( { licenseKey } ); - - sinon.assert.notCalled( showErrorStub ); - expect( editor.isReadOnly ).to.be.false; - } ); - - it( 'should not pass when domain is not in the licensedHosts list', () => { - const licenseKey = 'foo.eyJleHAiOjE3MTc1NDU2MDAsImp0aSI6Ijc2MWI4ZWQ2LWRmZTAtNGY0OS1hMTRkLWU2YzkxZjA4Y2ZjZSIsIm' + - 'xpY2Vuc2VkSG9zdHMiOlsiZmFjZWJvb2suY29tIl0sInZjIjoiNmEzNDdmYzYifQ.bar'; - - /** - * after decoding licenseKey: - * - * licensePaylod: { - * ..., - * licensedHosts: [ 'facebook.com' ] - * } - */ - - const editor = new TestEditor( { licenseKey } ); - - sinon.assert.calledWithMatch( showErrorStub, 'domainLimit' ); - expect( editor.isReadOnly ).to.be.true; - } ); - - it( 'should not pass if domain have no subdomain', () => { - const licenseKey = 'foo.eyJleHAiOjE3MTUzODU2MDAsImp0aSI6IjZmZGIxN2RhLTBiODAtNDI2Yi05ODA0LTc0NTEyNTZjMWE5N' + - 'yIsImxpY2Vuc2VkSG9zdHMiOlsiKi5sb2NhbGhvc3QiXSwidmMiOiJjNDMzYTk4OSJ9.bar'; - - /** - * after decoding licenseKey: - * - * licensePaylod: { - * ..., - * licensedHosts: [ '*.localhost' ] - * } - */ - - const editor = new TestEditor( { licenseKey } ); - - sinon.assert.calledWithMatch( showErrorStub, 'domainLimit' ); - expect( editor.isReadOnly ).to.be.true; - } ); - } ); - - describe( 'trial check', () => { - let consoleInfoSpy; - - beforeEach( () => { - sinon.useFakeTimers( { now: Date.now() } ); - consoleInfoSpy = sinon.spy( console, 'info' ); - } ); - - afterEach( () => { - sinon.restore(); - } ); - - it( 'should not block if trial is not expired', () => { - const licenseKey = 'foo.eyJleHAiOjE3MTUyMTI4MDAsImp0aSI6ImJkM2ZjNTc0LTJkNGYtNGNkZ' + - 'S1iNWViLTIzYzk1Y2JlMjQzYSIsImxpY2Vuc2VUeXBlIjoidHJpYWwiLCJ2YyI6ImZlOTdmNzY5In0.bar'; - - /** - * after decoding licenseKey: - * - * licensePaylod: { - * ..., - * exp: timestamp( 09.05.2024 ) - * licenseType: 'trial' - * } - */ - - const today = 1715166436000; // 08.05.2024 - const dateNow = sinon.stub( Date, 'now' ).returns( today ); - - const editor = new TestEditor( { licenseKey } ); - - sinon.assert.notCalled( showErrorStub ); - expect( editor.isReadOnly ).to.be.false; - - dateNow.restore(); - } ); - - it( 'should block if trial is expired', () => { - const licenseKey = 'foo.eyJleHAiOjE3MTUyMTI4MDAsImp0aSI6ImJkM2ZjNTc0LTJkNGYtNGNkZ' + - 'S1iNWViLTIzYzk1Y2JlMjQzYSIsImxpY2Vuc2VUeXBlIjoidHJpYWwiLCJ2YyI6ImZlOTdmNzY5In0.bar'; - - /** - * after decoding licenseKey: - * - * licensePaylod: { - * ..., - * exp: timestamp( 09.05.2024 ) - * licenseType: 'trial' - * } - */ - - const today = 1715339236000; // 10.05.2024 - const dateNow = sinon.stub( Date, 'now' ).returns( today ); - - const editor = new TestEditor( { licenseKey } ); - - sinon.assert.calledWithMatch( showErrorStub, 'trialLimit' ); - expect( editor.isReadOnly ).to.be.true; - sinon.assert.calledOnce( consoleInfoSpy ); - sinon.assert.calledWith( consoleInfoSpy, 'You are using the trial version of CKEditor 5 plugin with ' + - 'limited usage. Make sure you will not use it in the production environment.' ); - - dateNow.restore(); - } ); - - it( 'should block editor after 10 minutes if trial license.', () => { - const licenseKey = 'foo.eyJleHAiOjE3MTUyMTI4MDAsImp0aSI6ImJkM2ZjNTc0LTJkNGYtNGNkZ' + - 'S1iNWViLTIzYzk1Y2JlMjQzYSIsImxpY2Vuc2VUeXBlIjoidHJpYWwiLCJ2YyI6ImZlOTdmNzY5In0.bar'; - - /** - * after decoding licenseKey: - * - * licensePaylod: { - * ..., - * exp: timestamp( 09.05.2024 ) - * licenseType: 'trial' - * } - */ - - const today = 1715166436000; // 08.05.2024 - const dateNow = sinon.stub( Date, 'now' ).returns( today ); - - const editor = new TestEditor( { licenseKey } ); - - sinon.assert.notCalled( showErrorStub ); - expect( editor.isReadOnly ).to.be.false; - - sinon.clock.tick( 600100 ); - - sinon.assert.calledWithMatch( showErrorStub, 'trialLimit' ); - expect( editor.isReadOnly ).to.be.true; - sinon.assert.calledOnce( consoleInfoSpy ); - sinon.assert.calledWith( consoleInfoSpy, 'You are using the trial version of CKEditor 5 plugin with ' + - 'limited usage. Make sure you will not use it in the production environment.' ); - - dateNow.restore(); - } ); - - it( 'should clear timer on editor destroy', done => { - const licenseKey = 'foo.eyJleHAiOjE3MTUyMTI4MDAsImp0aSI6ImJkM2ZjNTc0LTJkNGYtNGNkZ' + - 'S1iNWViLTIzYzk1Y2JlMjQzYSIsImxpY2Vuc2VUeXBlIjoidHJpYWwiLCJ2YyI6ImZlOTdmNzY5In0.bar'; - - /** - * after decoding licenseKey: - * - * licensePaylod: { - * ..., - * exp: timestamp( 09.05.2024 ) - * licenseType: 'trial' - * } - */ - - const today = 1715166436000; // 08.05.2024 - const dateNow = sinon.stub( Date, 'now' ).returns( today ); - const editor = new TestEditor( { licenseKey } ); - const clearTimeoutSpy = sinon.spy( globalThis, 'clearTimeout' ); - - editor.fire( 'ready' ); - editor.on( 'destroy', () => { - sinon.assert.calledOnce( clearTimeoutSpy ); - done(); - } ); - - editor.destroy(); - dateNow.restore(); - } ); - } ); - - it( 'should block the editor when the license key is not valid (expiration date in the past)', () => { - const licenseKey = 'foo.eyJleHAiOjE3MDQwNjcyMDAsImp0aSI6ImZvbyIsInZjIjoiOTc4NTlGQkIifQo.bar'; - - const editor = new TestEditor( { licenseKey } ); - - sinon.assert.calledWithMatch( showErrorStub, 'expired' ); - expect( editor.isReadOnly ).to.be.true; - } ); - - it( 'should block the editor when the license key has wrong format (wrong verificationCode)', () => { - const licenseKey = 'foo.eyJleHAiOjIyMDg5ODg4MDAsImp0aSI6ImZvbyIsInZjIjoiMTIzNDU2NzgifQo.bar'; - - const editor = new TestEditor( { licenseKey } ); - - sinon.assert.calledWithMatch( showErrorStub, 'invalid' ); - expect( editor.isReadOnly ).to.be.true; - } ); - - it( 'should block the editor when the license key has wrong format (missing header part)', () => { - const licenseKey = 'eyJleHAiOjIyMDg5ODg4MDAsImp0aSI6ImZvbyIsInZjIjoiNDYyYTkzMGQifQ.bar'; - - const editor = new TestEditor( { licenseKey } ); - - sinon.assert.calledWithMatch( showErrorStub, 'invalid' ); - expect( editor.isReadOnly ).to.be.true; - } ); - - it( 'should block the editor when the license key has wrong format (payload does not start with `ey`)', () => { - const licenseKey = 'foo.JleHAiOjIyMDg5ODg4MDAsImp0aSI6ImZvbyIsInZlcmlmaWNhdGlvbkNvZGUiOiJjNTU2YWQ3NCJ9.bar'; - - const editor = new TestEditor( { licenseKey } ); - - sinon.assert.calledWithMatch( showErrorStub, 'invalid' ); - expect( editor.isReadOnly ).to.be.true; - } ); - - it( 'should block the editor when the license key has wrong format (payload not parsable as a JSON object)', () => { - const licenseKey = 'foo.eyZm9v.bar'; - - const editor = new TestEditor( { licenseKey } ); - - sinon.assert.calledWithMatch( showErrorStub, 'invalid' ); - expect( editor.isReadOnly ).to.be.true; - } ); - } ); - - describe( 'usage endpoint', () => { - it( 'should send request with telemetry data if license key contains a usage endpoint', () => { - const fetchStub = sinon.stub( window, 'fetch' ); - - // eslint-disable-next-line max-len - const licenseKey = 'foo.eyJleHAiOjM3ODY5MTIwMDAsImp0aSI6ImZvbyIsInVzYWdlRW5kcG9pbnQiOiJodHRwczovL2NrZWRpdG9yLmNvbSIsInZjIjoiYWI5NGFhZjYifQ.bar'; - const editor = new TestEditor( { licenseKey } ); - - editor.fire( 'ready' ); - - sinon.assert.calledOnce( fetchStub ); - - const sentData = JSON.parse( fetchStub.firstCall.lastArg.body ); - - expect( sentData.license ).to.equal( licenseKey ); - expect( sentData.telemetry ).to.deep.equal( { editorVersion: globalThis.CKEDITOR_VERSION } ); - } ); - - it( 'should not send any request if license key does not contain a usage endpoint', () => { - const fetchStub = sinon.stub( window, 'fetch' ); - - // eslint-disable-next-line max-len - const licenseKey = 'foo.eyJleHAiOjM3ODY5MTIwMDAsImp0aSI6ImZvbyIsInZjIjoiZjA3OTJhNjYifQ.bar'; - const editor = new TestEditor( { licenseKey } ); - - editor.fire( 'ready' ); - - sinon.assert.notCalled( fetchStub ); - } ); - - it( 'should display error on the console and not block the editor if response status is not ok (HTTP 500)', async () => { - const fetchStub = sinon.stub( window, 'fetch' ).resolves( new Response( null, { status: 500 } ) ); - const originalRejectionHandler = window.onunhandledrejection; - let capturedError = null; - - window.onunhandledrejection = evt => { - capturedError = evt.reason.message; - return true; - }; - - // eslint-disable-next-line max-len - const licenseKey = 'foo.eyJleHAiOjM3ODY5MTIwMDAsImp0aSI6ImZvbyIsInVzYWdlRW5kcG9pbnQiOiJodHRwczovL2NrZWRpdG9yLmNvbSIsInZjIjoiYWI5NGFhZjYifQ.bar'; - const editor = new TestEditor( { licenseKey } ); - - editor.fire( 'ready' ); - await wait( 1 ); - window.onunhandledrejection = originalRejectionHandler; - - sinon.assert.calledOnce( fetchStub ); - expect( capturedError ).to.equal( 'HTTP Response: 500' ); - expect( editor.isReadOnly ).to.be.false; - } ); - - it( 'should display warning and block the editor when usage status is not ok', async () => { - const fetchStub = sinon.stub( window, 'fetch' ).resolves( { - ok: true, - json: () => Promise.resolve( { - status: 'foo' - } ) - } ); - const showErrorStub = testUtils.sinon.stub( TestEditor.prototype, '_showLicenseError' ); - - // eslint-disable-next-line max-len - const licenseKey = 'foo.eyJleHAiOjM3ODY5MTIwMDAsImp0aSI6ImZvbyIsInVzYWdlRW5kcG9pbnQiOiJodHRwczovL2NrZWRpdG9yLmNvbSIsInZjIjoiYWI5NGFhZjYifQ.bar'; - const editor = new TestEditor( { licenseKey } ); - - editor.fire( 'ready' ); - await wait( 1 ); - - sinon.assert.calledOnce( fetchStub ); - sinon.assert.calledOnce( showErrorStub ); - sinon.assert.calledWithMatch( showErrorStub, 'usageLimit' ); - expect( editor.isReadOnly ).to.be.true; - } ); - - it( 'should display additional warning when usage status is not ok and message is provided', async () => { - const fetchStub = sinon.stub( window, 'fetch' ).resolves( { - ok: true, - json: () => Promise.resolve( { - status: 'foo', - message: 'bar' - } ) - } ); - const warnStub = testUtils.sinon.stub( console, 'warn' ); - const showErrorStub = testUtils.sinon.stub( TestEditor.prototype, '_showLicenseError' ); - - // eslint-disable-next-line max-len - const licenseKey = 'foo.eyJleHAiOjM3ODY5MTIwMDAsImp0aSI6ImZvbyIsInVzYWdlRW5kcG9pbnQiOiJodHRwczovL2NrZWRpdG9yLmNvbSIsInZjIjoiYWI5NGFhZjYifQ.bar'; - const editor = new TestEditor( { licenseKey } ); - - editor.fire( 'ready' ); - await wait( 1 ); - - sinon.assert.calledOnce( fetchStub ); - sinon.assert.calledOnce( warnStub ); - sinon.assert.calledOnce( showErrorStub ); - sinon.assert.calledWithMatch( warnStub, 'bar' ); - sinon.assert.calledWithMatch( showErrorStub, 'usageLimit' ); - expect( editor.isReadOnly ).to.be.true; - } ); - } ); } ); describe( 'context integration', () => { @@ -1732,9 +1353,3 @@ function getPlugins( editor ) { return Array.from( editor.plugins ) .map( entry => entry[ 1 ] ); // Get instances. } - -function wait( time ) { - return new Promise( res => { - window.setTimeout( res, time ); - } ); -} diff --git a/packages/ckeditor5-core/tests/editor/licensecheck.js b/packages/ckeditor5-core/tests/editor/licensecheck.js new file mode 100644 index 00000000000..6826eb0a37e --- /dev/null +++ b/packages/ckeditor5-core/tests/editor/licensecheck.js @@ -0,0 +1,441 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/* globals window, console, Response, globalThis, btoa */ + +import { releaseDate, crc32 } from '@ckeditor/ckeditor5-utils'; +import Editor from '../../src/editor/editor.js'; +import testUtils from '../../tests/_utils/utils.js'; + +class TestEditor extends Editor { + static create( config ) { + return new Promise( resolve => { + const editor = new this( config ); + + resolve( + editor.initPlugins() + .then( () => { + editor.fire( 'ready' ); + } ) + .then( () => editor ) + ); + } ); + } +} + +describe( 'License check', () => { + afterEach( () => { + delete TestEditor.builtinPlugins; + delete TestEditor.defaultConfig; + + sinon.restore(); + } ); + + describe( 'license key verification', () => { + let showErrorStub; + + beforeEach( () => { + showErrorStub = testUtils.sinon.stub( TestEditor.prototype, '_showLicenseError' ); + } ); + + describe( 'required fields in the license key', () => { + it( 'should not block the editor when required fields are provided and are valid', () => { + const { licenseKey } = generateKey(); + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.notCalled( showErrorStub ); + expect( editor.isReadOnly ).to.be.false; + } ); + + it( 'should block the editor when the `exp` field is missing', () => { + const { licenseKey } = generateKey( { expExist: false } ); + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.calledWithMatch( showErrorStub, 'invalid' ); + expect( editor.isReadOnly ).to.be.true; + } ); + + it( 'should block the editor when the `jti` field is missing', () => { + const { licenseKey } = generateKey( { jtiExist: false } ); + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.calledWithMatch( showErrorStub, 'invalid' ); + expect( editor.isReadOnly ).to.be.true; + } ); + + it( 'should block the editor when the `vc` field is missing', () => { + const { licenseKey } = generateKey( { vcExist: false } ); + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.calledWithMatch( showErrorStub, 'invalid' ); + expect( editor.isReadOnly ).to.be.true; + } ); + } ); + + describe( 'domain check', () => { + it( 'should pass when localhost is in the licensedHosts list', () => { + const { licenseKey } = generateKey( { licensedHosts: [ 'localhost' ] } ); + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.notCalled( showErrorStub ); + expect( editor.isReadOnly ).to.be.false; + } ); + + it( 'should not pass when domain is not in the licensedHosts list', () => { + const { licenseKey } = generateKey( { licensedHosts: [ 'facebook.com' ] } ); + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.calledWithMatch( showErrorStub, 'domainLimit' ); + expect( editor.isReadOnly ).to.be.true; + } ); + + it( 'should not pass if domain have no subdomain', () => { + const { licenseKey } = generateKey( { licensedHosts: [ '*.localhost' ] } ); + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.calledWithMatch( showErrorStub, 'domainLimit' ); + expect( editor.isReadOnly ).to.be.true; + } ); + } ); + + describe( 'trial check', () => { + let consoleInfoSpy; + + beforeEach( () => { + sinon.useFakeTimers( { now: Date.now() } ); + consoleInfoSpy = sinon.spy( console, 'info' ); + } ); + + afterEach( () => { + sinon.restore(); + } ); + + it( 'should not block if trial is not expired', () => { + const { licenseKey, todayTimestamp } = generateKey( { + licenseType: 'trial', + isExpired: false, + daysAfterExpiration: -1 + } ); + + const today = todayTimestamp; + const dateNow = sinon.stub( Date, 'now' ).returns( today ); + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.notCalled( showErrorStub ); + expect( editor.isReadOnly ).to.be.false; + + dateNow.restore(); + } ); + + it( 'should block if trial is expired', () => { + const { licenseKey, todayTimestamp } = generateKey( { + licenseType: 'trial', + isExpired: false, + daysAfterExpiration: 1 + } ); + + const dateNow = sinon.stub( Date, 'now' ).returns( todayTimestamp ); + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.calledWithMatch( showErrorStub, 'trialLimit' ); + expect( editor.isReadOnly ).to.be.true; + sinon.assert.calledOnce( consoleInfoSpy ); + sinon.assert.calledWith( consoleInfoSpy, 'You are using the trial version of CKEditor 5 plugin with ' + + 'limited usage. Make sure you will not use it in the production environment.' ); + + dateNow.restore(); + } ); + + it( 'should block editor after 10 minutes if trial license.', () => { + const { licenseKey, todayTimestamp } = generateKey( { + licenseType: 'trial', + isExpired: false, + daysAfterExpiration: -1 + } ); + + const dateNow = sinon.stub( Date, 'now' ).returns( todayTimestamp ); + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.notCalled( showErrorStub ); + expect( editor.isReadOnly ).to.be.false; + + sinon.clock.tick( 600100 ); + + sinon.assert.calledWithMatch( showErrorStub, 'trialLimit' ); + expect( editor.isReadOnly ).to.be.true; + sinon.assert.calledOnce( consoleInfoSpy ); + sinon.assert.calledWith( consoleInfoSpy, 'You are using the trial version of CKEditor 5 plugin with ' + + 'limited usage. Make sure you will not use it in the production environment.' ); + + dateNow.restore(); + } ); + + it( 'should clear timer on editor destroy', done => { + const { licenseKey, todayTimestamp } = generateKey( { + licenseType: 'trial', + isExpired: false, + daysAfterExpiration: -1 + } ); + + const dateNow = sinon.stub( Date, 'now' ).returns( todayTimestamp ); + const editor = new TestEditor( { licenseKey } ); + const clearTimeoutSpy = sinon.spy( globalThis, 'clearTimeout' ); + + editor.fire( 'ready' ); + editor.on( 'destroy', () => { + sinon.assert.calledOnce( clearTimeoutSpy ); + done(); + } ); + + editor.destroy(); + dateNow.restore(); + } ); + } ); + + it( 'should block the editor when the license key is not valid (expiration date in the past)', () => { + const { licenseKey } = generateKey( { + isExpired: true + } ); + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.calledWithMatch( showErrorStub, 'expired' ); + expect( editor.isReadOnly ).to.be.true; + } ); + + it( 'should block the editor when the license key has wrong format (wrong verificationCode)', () => { + const { licenseKey } = generateKey( { + isExpired: true, + vcForce: 'wrong vc' + } ); + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.calledWithMatch( showErrorStub, 'invalid' ); + expect( editor.isReadOnly ).to.be.true; + } ); + + it( 'should block the editor when the license key has wrong format (missing header part)', () => { + const { licenseKey } = generateKey( { + isExpired: true, + skipHeader: true + } ); + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.calledWithMatch( showErrorStub, 'invalid' ); + expect( editor.isReadOnly ).to.be.true; + } ); + + it( 'should block the editor when the license key has wrong format (missing tail part)', () => { + const { licenseKey } = generateKey( { + isExpired: true, + skipTail: true + } ); + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.calledWithMatch( showErrorStub, 'invalid' ); + expect( editor.isReadOnly ).to.be.true; + } ); + + it( 'should block the editor when the license key has wrong format (payload does not start with `ey`)', () => { + const licenseKey = 'foo.JleHAiOjIyMDg5ODg4MDAsImp0aSI6ImZvbyIsInZlcmlmaWNhdGlvbkNvZGUiOiJjNTU2YWQ3NCJ9.bar'; + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.calledWithMatch( showErrorStub, 'invalid' ); + expect( editor.isReadOnly ).to.be.true; + } ); + + it( 'should block the editor when the license key has wrong format (payload not parsable as a JSON object)', () => { + const licenseKey = 'foo.eyZm9v.bar'; + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.calledWithMatch( showErrorStub, 'invalid' ); + expect( editor.isReadOnly ).to.be.true; + } ); + } ); + + describe( 'usage endpoint', () => { + it( 'should send request with telemetry data if license key contains a usage endpoint', () => { + const fetchStub = sinon.stub( window, 'fetch' ); + + // eslint-disable-next-line max-len + const licenseKey = 'foo.eyJleHAiOjM3ODY5MTIwMDAsImp0aSI6ImZvbyIsInVzYWdlRW5kcG9pbnQiOiJodHRwczovL2NrZWRpdG9yLmNvbSIsInZjIjoiYWI5NGFhZjYifQ.bar'; + const editor = new TestEditor( { licenseKey } ); + + editor.fire( 'ready' ); + + sinon.assert.calledOnce( fetchStub ); + + const sentData = JSON.parse( fetchStub.firstCall.lastArg.body ); + + expect( sentData.license ).to.equal( licenseKey ); + expect( sentData.telemetry ).to.deep.equal( { editorVersion: globalThis.CKEDITOR_VERSION } ); + } ); + + it( 'should not send any request if license key does not contain a usage endpoint', () => { + const fetchStub = sinon.stub( window, 'fetch' ); + + // eslint-disable-next-line max-len + const licenseKey = 'foo.eyJleHAiOjM3ODY5MTIwMDAsImp0aSI6ImZvbyIsInZjIjoiZjA3OTJhNjYifQ.bar'; + const editor = new TestEditor( { licenseKey } ); + + editor.fire( 'ready' ); + + sinon.assert.notCalled( fetchStub ); + } ); + + it( 'should display error on the console and not block the editor if response status is not ok (HTTP 500)', async () => { + const fetchStub = sinon.stub( window, 'fetch' ).resolves( new Response( null, { status: 500 } ) ); + const originalRejectionHandler = window.onunhandledrejection; + let capturedError = null; + + window.onunhandledrejection = evt => { + capturedError = evt.reason.message; + return true; + }; + + // eslint-disable-next-line max-len + const licenseKey = 'foo.eyJleHAiOjM3ODY5MTIwMDAsImp0aSI6ImZvbyIsInVzYWdlRW5kcG9pbnQiOiJodHRwczovL2NrZWRpdG9yLmNvbSIsInZjIjoiYWI5NGFhZjYifQ.bar'; + const editor = new TestEditor( { licenseKey } ); + + editor.fire( 'ready' ); + await wait( 1 ); + window.onunhandledrejection = originalRejectionHandler; + + sinon.assert.calledOnce( fetchStub ); + expect( capturedError ).to.equal( 'HTTP Response: 500' ); + expect( editor.isReadOnly ).to.be.false; + } ); + + it( 'should display warning and block the editor when usage status is not ok', async () => { + const fetchStub = sinon.stub( window, 'fetch' ).resolves( { + ok: true, + json: () => Promise.resolve( { + status: 'foo' + } ) + } ); + const showErrorStub = testUtils.sinon.stub( TestEditor.prototype, '_showLicenseError' ); + + // eslint-disable-next-line max-len + const licenseKey = 'foo.eyJleHAiOjM3ODY5MTIwMDAsImp0aSI6ImZvbyIsInVzYWdlRW5kcG9pbnQiOiJodHRwczovL2NrZWRpdG9yLmNvbSIsInZjIjoiYWI5NGFhZjYifQ.bar'; + const editor = new TestEditor( { licenseKey } ); + + editor.fire( 'ready' ); + await wait( 1 ); + + sinon.assert.calledOnce( fetchStub ); + sinon.assert.calledOnce( showErrorStub ); + sinon.assert.calledWithMatch( showErrorStub, 'usageLimit' ); + expect( editor.isReadOnly ).to.be.true; + } ); + + it( 'should display additional warning when usage status is not ok and message is provided', async () => { + const fetchStub = sinon.stub( window, 'fetch' ).resolves( { + ok: true, + json: () => Promise.resolve( { + status: 'foo', + message: 'bar' + } ) + } ); + const warnStub = testUtils.sinon.stub( console, 'warn' ); + const showErrorStub = testUtils.sinon.stub( TestEditor.prototype, '_showLicenseError' ); + + // eslint-disable-next-line max-len + const licenseKey = 'foo.eyJleHAiOjM3ODY5MTIwMDAsImp0aSI6ImZvbyIsInVzYWdlRW5kcG9pbnQiOiJodHRwczovL2NrZWRpdG9yLmNvbSIsInZjIjoiYWI5NGFhZjYifQ.bar'; + const editor = new TestEditor( { licenseKey } ); + + editor.fire( 'ready' ); + await wait( 1 ); + + sinon.assert.calledOnce( fetchStub ); + sinon.assert.calledOnce( warnStub ); + sinon.assert.calledOnce( showErrorStub ); + sinon.assert.calledWithMatch( warnStub, 'bar' ); + sinon.assert.calledWithMatch( showErrorStub, 'usageLimit' ); + expect( editor.isReadOnly ).to.be.true; + } ); + } ); +} ); + +function wait( time ) { + return new Promise( res => { + window.setTimeout( res, time ); + } ); +} + +function generateKey( { + isExpired = false, + jtiExist = true, + expExist = true, + vcExist = true, + vcForce = undefined, + skipHeader, + skipTail, + daysAfterExpiration = 0, + licensedHosts, + licenseType +} ) { + const jti = 'foo'; + const releaseTimestamp = Date.parse( releaseDate ); + const day = 86400000; // one day in milliseconds. + + /** + * Depending on isExpired parameter we createing timestamp ten days + * before or after release day. + */ + const expirationTimestamp = isExpired ? releaseTimestamp - 10 * day : releaseTimestamp + 10 * day; + const todayTimestamp = ( expirationTimestamp + daysAfterExpiration * day ); + const vc = crc32( getCrcInputData( { + jti, + exp: expirationTimestamp / 1000, + licensedHosts, + licenseType + } ) ); + + const payload = encodePayload( { + jti: jtiExist && jti, + vc: vcForce && vcForce || vcExist && vc, + exp: expExist && expirationTimestamp / 1000, + licensedHosts, + licenseType + } ); + + return { + licenseKey: `${ skipHeader ? '' : 'foo.' }${ payload }${ skipTail ? '' : '.bar' }`, + todayTimestamp + }; +} + +function encodePayload( claims ) { + return encodeBase64Safe( JSON.stringify( claims ) ); +} + +function encodeBase64Safe( text ) { + return btoa( text ).replace( /\+/g, '-' ).replace( /\//g, '_' ).replace( /=+$/, '' ); +} + +function getCrcInputData( licensePayload ) { + const keysToCheck = Object.getOwnPropertyNames( licensePayload ).sort(); + + const filteredValues = keysToCheck + .filter( key => key != 'vc' && licensePayload[ key ] != null ) + .map( key => licensePayload[ key ] ); + + return [ ...filteredValues ]; +} From 85dc39523d2114175e0751b7580b72456ddf8523 Mon Sep 17 00:00:00 2001 From: Marta Motyczynska Date: Fri, 17 May 2024 15:42:28 +0200 Subject: [PATCH 034/256] Log information to the console about using the trial/dev license. Added/updated tests. --- packages/ckeditor5-core/src/editor/editor.ts | 15 ++--- .../ckeditor5-core/tests/editor/editor.js | 67 ++++++++++++++----- 2 files changed, 59 insertions(+), 23 deletions(-) diff --git a/packages/ckeditor5-core/src/editor/editor.ts b/packages/ckeditor5-core/src/editor/editor.ts index 4b8a87d7245..96e05a6544d 100644 --- a/packages/ckeditor5-core/src/editor/editor.ts +++ b/packages/ckeditor5-core/src/editor/editor.ts @@ -724,13 +724,17 @@ export default abstract class Editor extends ObservableMixin() { } } - if ( licensePayload.licenseType === 'trial' && licensePayload.exp * 1000 < Date.now() ) { - blockEditor( this, 'trialLimit' ); + if ( licensePayload.licenseType === 'trial' || licensePayload.licenseType === 'development' ) { + const licenseType: 'trial' | 'development' = licensePayload.licenseType; console.info( - 'You are using the trial version of CKEditor 5 with limited usage. ' + + `You are using the ${ licenseType } version of CKEditor 5 with limited usage. ` + 'Make sure you will not use it in the production environment.' ); + } + + if ( licensePayload.licenseType === 'trial' && licensePayload.exp * 1000 < Date.now() ) { + blockEditor( this, 'trialLimit' ); return; } @@ -740,11 +744,6 @@ export default abstract class Editor extends ObservableMixin() { const timerId = setTimeout( () => { blockEditor( this, `${ licenseType }Limit` ); - - console.info( - `You are using the ${ licenseType } version of CKEditor 5 with limited usage. ` + - 'Make sure you will not use it in the production environment.' - ); }, 600000 /* 10 minutes */ ); this.on( 'destroy', () => { diff --git a/packages/ckeditor5-core/tests/editor/editor.js b/packages/ckeditor5-core/tests/editor/editor.js index 543e654f89e..c1622540683 100644 --- a/packages/ckeditor5-core/tests/editor/editor.js +++ b/packages/ckeditor5-core/tests/editor/editor.js @@ -319,7 +319,7 @@ describe( 'Editor', () => { } ); } ); - describe( 'trial check', () => { + describe( 'trial license', () => { let consoleInfoSpy; beforeEach( () => { @@ -331,7 +331,21 @@ describe( 'Editor', () => { sinon.restore(); } ); - it( 'should not block if trial is not expired', () => { + it( 'should log information to the console about using the trial license', () => { + const licenseKey = 'foo.eyJleHAiOjE3MTUyMTI4MDAsImp0aSI6ImJkM2ZjNTc0LTJkNGYtNGNkZ' + + 'S1iNWViLTIzYzk1Y2JlMjQzYSIsImxpY2Vuc2VUeXBlIjoidHJpYWwiLCJ2YyI6ImZlOTdmNzY5In0.bar'; + + const today = 1715166436000; // 08.05.2024 + sinon.stub( Date, 'now' ).returns( today ); + const editor = new TestEditor( { licenseKey } ); + + expect( editor.isReadOnly ).to.be.false; + sinon.assert.calledOnce( consoleInfoSpy ); + sinon.assert.calledWith( consoleInfoSpy, 'You are using the trial version of CKEditor 5 with ' + + 'limited usage. Make sure you will not use it in the production environment.' ); + } ); + + it( 'should not block the editor if the trial has not expired', () => { const licenseKey = 'foo.eyJleHAiOjE3MTUyMTI4MDAsImp0aSI6ImJkM2ZjNTc0LTJkNGYtNGNkZ' + 'S1iNWViLTIzYzk1Y2JlMjQzYSIsImxpY2Vuc2VUeXBlIjoidHJpYWwiLCJ2YyI6ImZlOTdmNzY5In0.bar'; @@ -356,7 +370,7 @@ describe( 'Editor', () => { dateNow.restore(); } ); - it( 'should block if trial is expired', () => { + it( 'should block the editor if the trial has expired', () => { const licenseKey = 'foo.eyJleHAiOjE3MTUyMTI4MDAsImp0aSI6ImJkM2ZjNTc0LTJkNGYtNGNkZ' + 'S1iNWViLTIzYzk1Y2JlMjQzYSIsImxpY2Vuc2VUeXBlIjoidHJpYWwiLCJ2YyI6ImZlOTdmNzY5In0.bar'; @@ -377,14 +391,11 @@ describe( 'Editor', () => { sinon.assert.calledWithMatch( showErrorStub, 'trialLimit' ); expect( editor.isReadOnly ).to.be.true; - sinon.assert.calledOnce( consoleInfoSpy ); - sinon.assert.calledWith( consoleInfoSpy, 'You are using the trial version of CKEditor 5 with ' + - 'limited usage. Make sure you will not use it in the production environment.' ); dateNow.restore(); } ); - it( 'should block editor after 10 minutes if trial license.', () => { + it( 'should block editor after 10 minutes (trial license)', () => { const licenseKey = 'foo.eyJleHAiOjE3MTUyMTI4MDAsImp0aSI6ImJkM2ZjNTc0LTJkNGYtNGNkZ' + 'S1iNWViLTIzYzk1Y2JlMjQzYSIsImxpY2Vuc2VUeXBlIjoidHJpYWwiLCJ2YyI6ImZlOTdmNzY5In0.bar'; @@ -410,9 +421,6 @@ describe( 'Editor', () => { sinon.assert.calledWithMatch( showErrorStub, 'trialLimit' ); expect( editor.isReadOnly ).to.be.true; - sinon.assert.calledOnce( consoleInfoSpy ); - sinon.assert.calledWith( consoleInfoSpy, 'You are using the trial version of CKEditor 5 with ' + - 'limited usage. Make sure you will not use it in the production environment.' ); dateNow.restore(); } ); @@ -447,7 +455,7 @@ describe( 'Editor', () => { } ); } ); - describe( 'development check', () => { + describe( 'development license', () => { let consoleInfoSpy; beforeEach( () => { @@ -459,7 +467,39 @@ describe( 'Editor', () => { sinon.restore(); } ); - it( 'should block editor after 10 minutes if development license.', () => { + it( 'should log information to the console about using the development license', () => { + const licenseKey = 'foo.eyJleHAiOjE3MTUyMTI4MDAsImp0aSI6IjczNDk5YTQyLWJjNzktNDdlNy1hNmR' + + 'lLWIyMGJhMmEzYmI4OSIsImxpY2Vuc2VUeXBlIjoiZGV2ZWxvcG1lbnQiLCJ2YyI6Ijg5NzRiYTJlIn0.bar'; + + const editor = new TestEditor( { licenseKey } ); + + expect( editor.isReadOnly ).to.be.false; + sinon.assert.calledOnce( consoleInfoSpy ); + sinon.assert.calledWith( consoleInfoSpy, 'You are using the development version of CKEditor 5 with ' + + 'limited usage. Make sure you will not use it in the production environment.' ); + } ); + + it( 'should not block the editor if 10 minutes have not passed (development license)', () => { + const licenseKey = 'foo.eyJleHAiOjE3MTUyMTI4MDAsImp0aSI6IjczNDk5YTQyLWJjNzktNDdlNy1hNmR' + + 'lLWIyMGJhMmEzYmI4OSIsImxpY2Vuc2VUeXBlIjoiZGV2ZWxvcG1lbnQiLCJ2YyI6Ijg5NzRiYTJlIn0.bar'; + + const today = 1715166436000; // 08.05.2024 + const dateNow = sinon.stub( Date, 'now' ).returns( today ); + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.notCalled( showErrorStub ); + expect( editor.isReadOnly ).to.be.false; + + sinon.clock.tick( 1 ); + + sinon.assert.notCalled( showErrorStub ); + expect( editor.isReadOnly ).to.be.false; + + dateNow.restore(); + } ); + + it( 'should block editor after 10 minutes (development license)', () => { const licenseKey = 'foo.eyJleHAiOjE3MTUyMTI4MDAsImp0aSI6IjczNDk5YTQyLWJjNzktNDdlNy1hNmR' + 'lLWIyMGJhMmEzYmI4OSIsImxpY2Vuc2VUeXBlIjoiZGV2ZWxvcG1lbnQiLCJ2YyI6Ijg5NzRiYTJlIn0.bar'; @@ -485,9 +525,6 @@ describe( 'Editor', () => { sinon.assert.calledWithMatch( showErrorStub, 'developmentLimit' ); expect( editor.isReadOnly ).to.be.true; - sinon.assert.calledOnce( consoleInfoSpy ); - sinon.assert.calledWith( consoleInfoSpy, 'You are using the development version of CKEditor 5 with ' + - 'limited usage. Make sure you will not use it in the production environment.' ); dateNow.restore(); } ); From 45d68c9addba70a42731867c563fc90f215f10e5 Mon Sep 17 00:00:00 2001 From: Marta Motyczynska Date: Fri, 17 May 2024 15:45:04 +0200 Subject: [PATCH 035/256] Remove unnecessary license key from manual test. --- packages/ckeditor5-font/tests/manual/font-color.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/ckeditor5-font/tests/manual/font-color.js b/packages/ckeditor5-font/tests/manual/font-color.js index d93487bba72..b5823f5a928 100644 --- a/packages/ckeditor5-font/tests/manual/font-color.js +++ b/packages/ckeditor5-font/tests/manual/font-color.js @@ -34,9 +34,7 @@ ClassicEditor ], fontColor: { columns: 3 - }, - licenseKey: 'foo.eyJleHAiOjE3MTY0MjI0MDAsImp0aSI6ImY5ZTViYjc5LTYzZTgtNGE0NS05YWQxLTg5YjBiNm' + - 'ZlNjE3MyIsImxpY2Vuc2VUeXBlIjoiZGV2ZWxvcG1lbnQiLCJ2YyI6ImU4NzdmNGE3In0.bar' + } } ) .then( editor => { window.editor = editor; From 0d312b1463df866f5445aa7a72fdbea4b626a49e Mon Sep 17 00:00:00 2001 From: Marta Motyczynska Date: Mon, 20 May 2024 11:30:52 +0200 Subject: [PATCH 036/256] Revert changes in poweredby. --- .../ckeditor5-ui/src/editorui/poweredby.ts | 11 ++-------- .../ckeditor5-ui/tests/editorui/poweredby.js | 22 ------------------- 2 files changed, 2 insertions(+), 31 deletions(-) diff --git a/packages/ckeditor5-ui/src/editorui/poweredby.ts b/packages/ckeditor5-ui/src/editorui/poweredby.ts index 623d20248cd..7b858d2e0cd 100644 --- a/packages/ckeditor5-ui/src/editorui/poweredby.ts +++ b/packages/ckeditor5-ui/src/editorui/poweredby.ts @@ -106,7 +106,7 @@ export default class PoweredBy extends DomEmitterMixin() { const licenseKey = editor.config.get( 'licenseKey' ); const licenseContent = licenseKey && parseBase64EncodedObject( licenseKey.split( '.' )[ 1 ] ); - if ( licenseContent && ( licenseContent.whiteLabel || licenseContent.licenseType !== 'development' ) ) { + if ( licenseContent && licenseContent.whiteLabel ) { return; } } @@ -357,17 +357,10 @@ function getLowerCornerPosition( function getNormalizedConfig( editor: Editor ): PoweredByConfig { const userConfig = editor.config.get( 'ui.poweredBy' ); const position = userConfig && userConfig.position || 'border'; - const licenseKey = editor.config.get( 'licenseKey' ); - const licenseContent = licenseKey && parseBase64EncodedObject( licenseKey.split( '.' )[ 1 ] ); - let label = DEFAULT_LABEL; - - if ( licenseContent && licenseContent.licenseType === 'development' ) { - label = 'Development'; - } return { position, - label, + label: DEFAULT_LABEL, verticalOffset: position === 'inside' ? 5 : 0, horizontalOffset: 5, diff --git a/packages/ckeditor5-ui/tests/editorui/poweredby.js b/packages/ckeditor5-ui/tests/editorui/poweredby.js index 53bee5bb05f..56d5a60d5c4 100644 --- a/packages/ckeditor5-ui/tests/editorui/poweredby.js +++ b/packages/ckeditor5-ui/tests/editorui/poweredby.js @@ -129,28 +129,6 @@ describe( 'PoweredBy', () => { await editor.destroy(); } ); - - it( 'should create the balloon when a development license key is configured', async () => { - const editor = await createEditor( element, { - licenseKey: 'foo.eyJleHAiOjE3MTUyMTI4MDAsImp0aSI6IjczNDk5YTQyLWJjNzktNDdlNy1hNmR' + - 'lLWIyMGJhMmEzYmI4OSIsImxpY2Vuc2VUeXBlIjoiZGV2ZWxvcG1lbnQiLCJ2YyI6Ijg5NzRiYTJlIn0.bar' - } ); - - expect( editor.ui.poweredBy._balloonView ).to.be.null; - - focusEditor( editor ); - - expect( editor.ui.poweredBy._balloonView ).to.be.instanceOf( BalloonPanelView ); - - const view = editor.ui.poweredBy._balloonView.content.first; - - expect( view.element.firstChild.firstChild.tagName ).to.equal( 'SPAN' ); - expect( view.element.firstChild.firstChild.classList.contains( 'ck' ) ).to.be.true; - expect( view.element.firstChild.firstChild.classList.contains( 'ck-powered-by__label' ) ).to.be.true; - expect( view.element.firstChild.firstChild.textContent ).to.equal( 'Development' ); - - await editor.destroy(); - } ); } ); describe( 'balloon management on editor focus change', () => { From a7854a9a00ccf64745bcf9bb607cd7d332ab9939 Mon Sep 17 00:00:00 2001 From: Illia Sheremetov Date: Mon, 20 May 2024 14:55:17 +0200 Subject: [PATCH 037/256] Adjust tests to new helper. --- .../tests/editor/licensecheck.js | 41 +++++++++++-------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/packages/ckeditor5-core/tests/editor/licensecheck.js b/packages/ckeditor5-core/tests/editor/licensecheck.js index 6826eb0a37e..4e97f7b3114 100644 --- a/packages/ckeditor5-core/tests/editor/licensecheck.js +++ b/packages/ckeditor5-core/tests/editor/licensecheck.js @@ -217,8 +217,7 @@ describe( 'License check', () => { it( 'should block the editor when the license key has wrong format (wrong verificationCode)', () => { const { licenseKey } = generateKey( { - isExpired: true, - vcForce: 'wrong vc' + customVc: 'wrong vc' } ); const editor = new TestEditor( { licenseKey } ); @@ -274,8 +273,9 @@ describe( 'License check', () => { it( 'should send request with telemetry data if license key contains a usage endpoint', () => { const fetchStub = sinon.stub( window, 'fetch' ); - // eslint-disable-next-line max-len - const licenseKey = 'foo.eyJleHAiOjM3ODY5MTIwMDAsImp0aSI6ImZvbyIsInVzYWdlRW5kcG9pbnQiOiJodHRwczovL2NrZWRpdG9yLmNvbSIsInZjIjoiYWI5NGFhZjYifQ.bar'; + const { licenseKey } = generateKey( { + usageEndpoint: 'https://ckeditor.com' + } ); const editor = new TestEditor( { licenseKey } ); editor.fire( 'ready' ); @@ -291,8 +291,7 @@ describe( 'License check', () => { it( 'should not send any request if license key does not contain a usage endpoint', () => { const fetchStub = sinon.stub( window, 'fetch' ); - // eslint-disable-next-line max-len - const licenseKey = 'foo.eyJleHAiOjM3ODY5MTIwMDAsImp0aSI6ImZvbyIsInZjIjoiZjA3OTJhNjYifQ.bar'; + const { licenseKey } = generateKey(); const editor = new TestEditor( { licenseKey } ); editor.fire( 'ready' ); @@ -310,8 +309,9 @@ describe( 'License check', () => { return true; }; - // eslint-disable-next-line max-len - const licenseKey = 'foo.eyJleHAiOjM3ODY5MTIwMDAsImp0aSI6ImZvbyIsInVzYWdlRW5kcG9pbnQiOiJodHRwczovL2NrZWRpdG9yLmNvbSIsInZjIjoiYWI5NGFhZjYifQ.bar'; + const { licenseKey } = generateKey( { + usageEndpoint: 'https://ckeditor.com' + } ); const editor = new TestEditor( { licenseKey } ); editor.fire( 'ready' ); @@ -332,8 +332,9 @@ describe( 'License check', () => { } ); const showErrorStub = testUtils.sinon.stub( TestEditor.prototype, '_showLicenseError' ); - // eslint-disable-next-line max-len - const licenseKey = 'foo.eyJleHAiOjM3ODY5MTIwMDAsImp0aSI6ImZvbyIsInVzYWdlRW5kcG9pbnQiOiJodHRwczovL2NrZWRpdG9yLmNvbSIsInZjIjoiYWI5NGFhZjYifQ.bar'; + const { licenseKey } = generateKey( { + usageEndpoint: 'https://ckeditor.com' + } ); const editor = new TestEditor( { licenseKey } ); editor.fire( 'ready' ); @@ -356,8 +357,9 @@ describe( 'License check', () => { const warnStub = testUtils.sinon.stub( console, 'warn' ); const showErrorStub = testUtils.sinon.stub( TestEditor.prototype, '_showLicenseError' ); - // eslint-disable-next-line max-len - const licenseKey = 'foo.eyJleHAiOjM3ODY5MTIwMDAsImp0aSI6ImZvbyIsInVzYWdlRW5kcG9pbnQiOiJodHRwczovL2NrZWRpdG9yLmNvbSIsInZjIjoiYWI5NGFhZjYifQ.bar'; + const { licenseKey } = generateKey( { + usageEndpoint: 'https://ckeditor.com' + } ); const editor = new TestEditor( { licenseKey } ); editor.fire( 'ready' ); @@ -384,13 +386,14 @@ function generateKey( { jtiExist = true, expExist = true, vcExist = true, - vcForce = undefined, + customVc = undefined, skipHeader, skipTail, daysAfterExpiration = 0, licensedHosts, - licenseType -} ) { + licenseType, + usageEndpoint +} = {} ) { const jti = 'foo'; const releaseTimestamp = Date.parse( releaseDate ); const day = 86400000; // one day in milliseconds. @@ -405,15 +408,17 @@ function generateKey( { jti, exp: expirationTimestamp / 1000, licensedHosts, - licenseType + licenseType, + usageEndpoint } ) ); const payload = encodePayload( { jti: jtiExist && jti, - vc: vcForce && vcForce || vcExist && vc, + vc: ( customVc && customVc ) || ( vcExist ? vc : undefined ), exp: expExist && expirationTimestamp / 1000, licensedHosts, - licenseType + licenseType, + usageEndpoint } ); return { From 6ba9c4fe8f84021120dd2f3220ea59cb5bb133f8 Mon Sep 17 00:00:00 2001 From: Illia Sheremetov Date: Tue, 21 May 2024 12:04:44 +0200 Subject: [PATCH 038/256] Adjust development test to new license helper. --- .../tests/editor/licensecheck.js | 51 ++++++------------- 1 file changed, 16 insertions(+), 35 deletions(-) diff --git a/packages/ckeditor5-core/tests/editor/licensecheck.js b/packages/ckeditor5-core/tests/editor/licensecheck.js index a4d8d9d05ca..ea006a8793a 100644 --- a/packages/ckeditor5-core/tests/editor/licensecheck.js +++ b/packages/ckeditor5-core/tests/editor/licensecheck.js @@ -140,7 +140,6 @@ describe( 'License check', () => { it( 'should block if trial is expired', () => { const { licenseKey, todayTimestamp } = generateKey( { licenseType: 'trial', - isExpired: false, daysAfterExpiration: 1 } ); @@ -151,7 +150,7 @@ describe( 'License check', () => { sinon.assert.calledWithMatch( showErrorStub, 'trialLimit' ); expect( editor.isReadOnly ).to.be.true; sinon.assert.calledOnce( consoleInfoSpy ); - sinon.assert.calledWith( consoleInfoSpy, 'You are using the trial version of CKEditor 5 plugin with ' + + sinon.assert.calledWith( consoleInfoSpy, 'You are using the trial version of CKEditor 5 with ' + 'limited usage. Make sure you will not use it in the production environment.' ); dateNow.restore(); @@ -176,7 +175,7 @@ describe( 'License check', () => { sinon.assert.calledWithMatch( showErrorStub, 'trialLimit' ); expect( editor.isReadOnly ).to.be.true; sinon.assert.calledOnce( consoleInfoSpy ); - sinon.assert.calledWith( consoleInfoSpy, 'You are using the trial version of CKEditor 5 plugin with ' + + sinon.assert.calledWith( consoleInfoSpy, 'You are using the trial version of CKEditor 5 with ' + 'limited usage. Make sure you will not use it in the production environment.' ); dateNow.restore(); @@ -217,8 +216,9 @@ describe( 'License check', () => { } ); it( 'should log information to the console about using the development license', () => { - const licenseKey = 'foo.eyJleHAiOjE3MTUyMTI4MDAsImp0aSI6IjczNDk5YTQyLWJjNzktNDdlNy1hNmR' + - 'lLWIyMGJhMmEzYmI4OSIsImxpY2Vuc2VUeXBlIjoiZGV2ZWxvcG1lbnQiLCJ2YyI6Ijg5NzRiYTJlIn0.bar'; + const { licenseKey } = generateKey( { + licenseType: 'development' + } ); const editor = new TestEditor( { licenseKey } ); @@ -229,8 +229,9 @@ describe( 'License check', () => { } ); it( 'should not block the editor if 10 minutes have not passed (development license)', () => { - const licenseKey = 'foo.eyJleHAiOjE3MTUyMTI4MDAsImp0aSI6IjczNDk5YTQyLWJjNzktNDdlNy1hNmR' + - 'lLWIyMGJhMmEzYmI4OSIsImxpY2Vuc2VUeXBlIjoiZGV2ZWxvcG1lbnQiLCJ2YyI6Ijg5NzRiYTJlIn0.bar'; + const { licenseKey } = generateKey( { + licenseType: 'development' + } ); const today = 1715166436000; // 08.05.2024 const dateNow = sinon.stub( Date, 'now' ).returns( today ); @@ -249,21 +250,11 @@ describe( 'License check', () => { } ); it( 'should block editor after 10 minutes (development license)', () => { - const licenseKey = 'foo.eyJleHAiOjE3MTUyMTI4MDAsImp0aSI6IjczNDk5YTQyLWJjNzktNDdlNy1hNmR' + - 'lLWIyMGJhMmEzYmI4OSIsImxpY2Vuc2VUeXBlIjoiZGV2ZWxvcG1lbnQiLCJ2YyI6Ijg5NzRiYTJlIn0.bar'; - - /** - * after decoding licenseKey: - * - * licensePaylod: { - * ..., - * exp: timestamp( 09.05.2024 ) - * licenseType: 'development' - * } - */ + const { licenseKey, todayTimestamp } = generateKey( { + licenseType: 'development' + } ); - const today = 1715166436000; // 08.05.2024 - const dateNow = sinon.stub( Date, 'now' ).returns( today ); + const dateNow = sinon.stub( Date, 'now' ).returns( todayTimestamp ); const editor = new TestEditor( { licenseKey } ); @@ -279,21 +270,11 @@ describe( 'License check', () => { } ); it( 'should clear timer on editor destroy', done => { - const licenseKey = 'foo.eyJleHAiOjE3MTUyMTI4MDAsImp0aSI6IjczNDk5YTQyLWJjNzktNDdlNy1hNmR' + - 'lLWIyMGJhMmEzYmI4OSIsImxpY2Vuc2VUeXBlIjoiZGV2ZWxvcG1lbnQiLCJ2YyI6Ijg5NzRiYTJlIn0.bar'; - - /** - * after decoding licenseKey: - * - * licensePaylod: { - * ..., - * exp: timestamp( 09.05.2024 ) - * licenseType: 'development' - * } - */ + const { licenseKey, todayTimestamp } = generateKey( { + licenseType: 'development' + } ); - const today = 1715166436000; // 08.05.2024 - const dateNow = sinon.stub( Date, 'now' ).returns( today ); + const dateNow = sinon.stub( Date, 'now' ).returns( todayTimestamp ); const editor = new TestEditor( { licenseKey } ); const clearTimeoutSpy = sinon.spy( globalThis, 'clearTimeout' ); From 7b761d93ed45fe6a812a84d52834cc6f3f966efb Mon Sep 17 00:00:00 2001 From: Illia Sheremetov Date: Tue, 21 May 2024 12:12:01 +0200 Subject: [PATCH 039/256] Fix typo. --- packages/ckeditor5-core/tests/editor/licensecheck.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ckeditor5-core/tests/editor/licensecheck.js b/packages/ckeditor5-core/tests/editor/licensecheck.js index ea006a8793a..4b75fde8f6e 100644 --- a/packages/ckeditor5-core/tests/editor/licensecheck.js +++ b/packages/ckeditor5-core/tests/editor/licensecheck.js @@ -484,7 +484,7 @@ function generateKey( { const day = 86400000; // one day in milliseconds. /** - * Depending on isExpired parameter we createing timestamp ten days + * Depending on isExpired parameter we are creating timestamp ten days * before or after release day. */ const expirationTimestamp = isExpired ? releaseTimestamp - 10 * day : releaseTimestamp + 10 * day; From 87a0deb2b8ece17486b7d7f87cd02dc789aa6fa2 Mon Sep 17 00:00:00 2001 From: Arkadiusz Filipczak Date: Tue, 21 May 2024 16:11:17 +0200 Subject: [PATCH 040/256] License v3 - network error reporting. --- packages/ckeditor5-core/src/editor/editor.ts | 24 +++++---- .../ckeditor5-core/tests/editor/editor.js | 52 ++++++++----------- 2 files changed, 38 insertions(+), 38 deletions(-) diff --git a/packages/ckeditor5-core/src/editor/editor.ts b/packages/ckeditor5-core/src/editor/editor.ts index 96e05a6544d..82251178489 100644 --- a/packages/ckeditor5-core/src/editor/editor.ts +++ b/packages/ckeditor5-core/src/editor/editor.ts @@ -11,6 +11,7 @@ import { Config, CKEditorError, ObservableMixin, + logError, parseBase64EncodedObject, releaseDate, uid, @@ -724,15 +725,6 @@ export default abstract class Editor extends ObservableMixin() { } } - if ( licensePayload.licenseType === 'trial' || licensePayload.licenseType === 'development' ) { - const licenseType: 'trial' | 'development' = licensePayload.licenseType; - - console.info( - `You are using the ${ licenseType } version of CKEditor 5 with limited usage. ` + - 'Make sure you will not use it in the production environment.' - ); - } - if ( licensePayload.licenseType === 'trial' && licensePayload.exp * 1000 < Date.now() ) { blockEditor( this, 'trialLimit' ); @@ -742,6 +734,11 @@ export default abstract class Editor extends ObservableMixin() { if ( licensePayload.licenseType === 'trial' || licensePayload.licenseType === 'development' ) { const licenseType: 'trial' | 'development' = licensePayload.licenseType; + console.info( + `You are using the ${ licenseType } version of CKEditor 5 with limited usage. ` + + 'Make sure you will not use it in the production environment.' + ); + const timerId = setTimeout( () => { blockEditor( this, `${ licenseType }Limit` ); }, 600000 /* 10 minutes */ ); @@ -772,6 +769,15 @@ export default abstract class Editor extends ObservableMixin() { if ( status != 'ok' ) { blockEditor( this, 'usageLimit' ); } + }, () => { + /** + * Your license key cannot be validated because of a network issue. + * Please make sure that your setup does not block the request. + * + * @error license-key-validaton-endpoint-not-reachable + * @param {String} url The URL that was attempted to reach. + */ + logError( 'license-key-validaton-endpoint-not-reachable', { url: licensePayload.usageEndpoint } ); } ); }, { priority: 'high' } ); } diff --git a/packages/ckeditor5-core/tests/editor/editor.js b/packages/ckeditor5-core/tests/editor/editor.js index c1622540683..cd5c8de6663 100644 --- a/packages/ckeditor5-core/tests/editor/editor.js +++ b/packages/ckeditor5-core/tests/editor/editor.js @@ -469,7 +469,7 @@ describe( 'Editor', () => { it( 'should log information to the console about using the development license', () => { const licenseKey = 'foo.eyJleHAiOjE3MTUyMTI4MDAsImp0aSI6IjczNDk5YTQyLWJjNzktNDdlNy1hNmR' + - 'lLWIyMGJhMmEzYmI4OSIsImxpY2Vuc2VUeXBlIjoiZGV2ZWxvcG1lbnQiLCJ2YyI6Ijg5NzRiYTJlIn0.bar'; + 'lLWIyMGJhMmEzYmI4OSIsImxpY2Vuc2VUeXBlIjoiZGV2ZWxvcG1lbnQiLCJ2YyI6Ijg5NzRiYTJlIn0.bar'; const editor = new TestEditor( { licenseKey } ); @@ -481,7 +481,7 @@ describe( 'Editor', () => { it( 'should not block the editor if 10 minutes have not passed (development license)', () => { const licenseKey = 'foo.eyJleHAiOjE3MTUyMTI4MDAsImp0aSI6IjczNDk5YTQyLWJjNzktNDdlNy1hNmR' + - 'lLWIyMGJhMmEzYmI4OSIsImxpY2Vuc2VUeXBlIjoiZGV2ZWxvcG1lbnQiLCJ2YyI6Ijg5NzRiYTJlIn0.bar'; + 'lLWIyMGJhMmEzYmI4OSIsImxpY2Vuc2VUeXBlIjoiZGV2ZWxvcG1lbnQiLCJ2YyI6Ijg5NzRiYTJlIn0.bar'; const today = 1715166436000; // 08.05.2024 const dateNow = sinon.stub( Date, 'now' ).returns( today ); @@ -501,17 +501,17 @@ describe( 'Editor', () => { it( 'should block editor after 10 minutes (development license)', () => { const licenseKey = 'foo.eyJleHAiOjE3MTUyMTI4MDAsImp0aSI6IjczNDk5YTQyLWJjNzktNDdlNy1hNmR' + - 'lLWIyMGJhMmEzYmI4OSIsImxpY2Vuc2VUeXBlIjoiZGV2ZWxvcG1lbnQiLCJ2YyI6Ijg5NzRiYTJlIn0.bar'; + 'lLWIyMGJhMmEzYmI4OSIsImxpY2Vuc2VUeXBlIjoiZGV2ZWxvcG1lbnQiLCJ2YyI6Ijg5NzRiYTJlIn0.bar'; /** - * after decoding licenseKey: - * - * licensePaylod: { - * ..., - * exp: timestamp( 09.05.2024 ) - * licenseType: 'development' - * } - */ + * after decoding licenseKey: + * + * licensePaylod: { + * ..., + * exp: timestamp( 09.05.2024 ) + * licenseType: 'development' + * } + */ const today = 1715166436000; // 08.05.2024 const dateNow = sinon.stub( Date, 'now' ).returns( today ); @@ -531,17 +531,17 @@ describe( 'Editor', () => { it( 'should clear timer on editor destroy', done => { const licenseKey = 'foo.eyJleHAiOjE3MTUyMTI4MDAsImp0aSI6IjczNDk5YTQyLWJjNzktNDdlNy1hNmR' + - 'lLWIyMGJhMmEzYmI4OSIsImxpY2Vuc2VUeXBlIjoiZGV2ZWxvcG1lbnQiLCJ2YyI6Ijg5NzRiYTJlIn0.bar'; + 'lLWIyMGJhMmEzYmI4OSIsImxpY2Vuc2VUeXBlIjoiZGV2ZWxvcG1lbnQiLCJ2YyI6Ijg5NzRiYTJlIn0.bar'; /** - * after decoding licenseKey: - * - * licensePaylod: { - * ..., - * exp: timestamp( 09.05.2024 ) - * licenseType: 'development' - * } - */ + * after decoding licenseKey: + * + * licensePaylod: { + * ..., + * exp: timestamp( 09.05.2024 ) + * licenseType: 'development' + * } + */ const today = 1715166436000; // 08.05.2024 const dateNow = sinon.stub( Date, 'now' ).returns( today ); @@ -637,13 +637,7 @@ describe( 'Editor', () => { it( 'should display error on the console and not block the editor if response status is not ok (HTTP 500)', async () => { const fetchStub = sinon.stub( window, 'fetch' ).resolves( new Response( null, { status: 500 } ) ); - const originalRejectionHandler = window.onunhandledrejection; - let capturedError = null; - - window.onunhandledrejection = evt => { - capturedError = evt.reason.message; - return true; - }; + const errorStub = sinon.stub( console, 'error' ); // eslint-disable-next-line max-len const licenseKey = 'foo.eyJleHAiOjM3ODY5MTIwMDAsImp0aSI6ImZvbyIsInVzYWdlRW5kcG9pbnQiOiJodHRwczovL2NrZWRpdG9yLmNvbSIsInZjIjoiYWI5NGFhZjYifQ.bar'; @@ -651,10 +645,10 @@ describe( 'Editor', () => { editor.fire( 'ready' ); await wait( 1 ); - window.onunhandledrejection = originalRejectionHandler; sinon.assert.calledOnce( fetchStub ); - expect( capturedError ).to.equal( 'HTTP Response: 500' ); + sinon.assert.calledWithMatch( + errorStub, 'license-key-validaton-endpoint-not-reachable', { 'url': 'https://ckeditor.com' } ); expect( editor.isReadOnly ).to.be.false; } ); From 9a99ecfc102345151e65bc42eae72a5205ac3977 Mon Sep 17 00:00:00 2001 From: Illia Sheremetov Date: Wed, 22 May 2024 14:09:06 +0200 Subject: [PATCH 041/256] Code review fixes. --- packages/ckeditor5-core/src/editor/editor.ts | 9 +++++-- .../tests/editor/licensecheck.js | 24 +++++++------------ 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/packages/ckeditor5-core/src/editor/editor.ts b/packages/ckeditor5-core/src/editor/editor.ts index 82251178489..44c3609178a 100644 --- a/packages/ckeditor5-core/src/editor/editor.ts +++ b/packages/ckeditor5-core/src/editor/editor.ts @@ -725,15 +725,20 @@ export default abstract class Editor extends ObservableMixin() { } } + const licenseType: 'trial' | 'development' = licensePayload.licenseType; + if ( licensePayload.licenseType === 'trial' && licensePayload.exp * 1000 < Date.now() ) { blockEditor( this, 'trialLimit' ); + console.info( + `You are using the ${ licenseType } version of CKEditor 5 with limited usage. ` + + 'Make sure you will not use it in the production environment.' + ); + return; } if ( licensePayload.licenseType === 'trial' || licensePayload.licenseType === 'development' ) { - const licenseType: 'trial' | 'development' = licensePayload.licenseType; - console.info( `You are using the ${ licenseType } version of CKEditor 5 with limited usage. ` + 'Make sure you will not use it in the production environment.' diff --git a/packages/ckeditor5-core/tests/editor/licensecheck.js b/packages/ckeditor5-core/tests/editor/licensecheck.js index 4b75fde8f6e..e32e7ccf068 100644 --- a/packages/ckeditor5-core/tests/editor/licensecheck.js +++ b/packages/ckeditor5-core/tests/editor/licensecheck.js @@ -385,26 +385,20 @@ describe( 'License check', () => { } ); it( 'should display error on the console and not block the editor if response status is not ok (HTTP 500)', async () => { - const fetchStub = sinon.stub( window, 'fetch' ).resolves( new Response( null, { status: 500 } ) ); - const originalRejectionHandler = window.onunhandledrejection; - let capturedError = null; - - window.onunhandledrejection = evt => { - capturedError = evt.reason.message; - return true; - }; - const { licenseKey } = generateKey( { usageEndpoint: 'https://ckeditor.com' } ); + const fetchStub = sinon.stub( window, 'fetch' ).resolves( new Response( null, { status: 500 } ) ); + const errorStub = sinon.stub( console, 'error' ); + const editor = new TestEditor( { licenseKey } ); editor.fire( 'ready' ); await wait( 1 ); - window.onunhandledrejection = originalRejectionHandler; sinon.assert.calledOnce( fetchStub ); - expect( capturedError ).to.equal( 'HTTP Response: 500' ); + sinon.assert.calledWithMatch( + errorStub, 'license-key-validaton-endpoint-not-reachable', { 'url': 'https://ckeditor.com' } ); expect( editor.isReadOnly ).to.be.false; } ); @@ -484,9 +478,9 @@ function generateKey( { const day = 86400000; // one day in milliseconds. /** - * Depending on isExpired parameter we are creating timestamp ten days - * before or after release day. - */ + * Depending on isExpired parameter we are creating timestamp ten days + * before or after release day. + */ const expirationTimestamp = isExpired ? releaseTimestamp - 10 * day : releaseTimestamp + 10 * day; const todayTimestamp = ( expirationTimestamp + daysAfterExpiration * day ); const vc = crc32( getCrcInputData( { @@ -499,7 +493,7 @@ function generateKey( { const payload = encodePayload( { jti: jtiExist && jti, - vc: ( customVc && customVc ) || ( vcExist ? vc : undefined ), + vc: customVc || ( vcExist ? vc : undefined ), exp: expExist && expirationTimestamp / 1000, licensedHosts, licenseType, From 1d49480674b62c176f6740e7933fbb06e8be2dcf Mon Sep 17 00:00:00 2001 From: Illia Sheremetov Date: Wed, 22 May 2024 14:21:12 +0200 Subject: [PATCH 042/256] Fix comment. --- packages/ckeditor5-core/tests/editor/licensecheck.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/ckeditor5-core/tests/editor/licensecheck.js b/packages/ckeditor5-core/tests/editor/licensecheck.js index e32e7ccf068..68df6fee933 100644 --- a/packages/ckeditor5-core/tests/editor/licensecheck.js +++ b/packages/ckeditor5-core/tests/editor/licensecheck.js @@ -477,10 +477,8 @@ function generateKey( { const releaseTimestamp = Date.parse( releaseDate ); const day = 86400000; // one day in milliseconds. - /** - * Depending on isExpired parameter we are creating timestamp ten days - * before or after release day. - */ + // Depending on isExpired parameter we are creating timestamp ten days + // before or after release day. const expirationTimestamp = isExpired ? releaseTimestamp - 10 * day : releaseTimestamp + 10 * day; const todayTimestamp = ( expirationTimestamp + daysAfterExpiration * day ); const vc = crc32( getCrcInputData( { From 4bc668f576f8b329f1da84617e4786fc93103e18 Mon Sep 17 00:00:00 2001 From: Illia Sheremetov Date: Wed, 22 May 2024 15:54:06 +0200 Subject: [PATCH 043/256] Test fix. --- packages/ckeditor5-core/src/editor/editor.ts | 9 ++------- packages/ckeditor5-core/tests/editor/licensecheck.js | 3 --- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/packages/ckeditor5-core/src/editor/editor.ts b/packages/ckeditor5-core/src/editor/editor.ts index 44c3609178a..82251178489 100644 --- a/packages/ckeditor5-core/src/editor/editor.ts +++ b/packages/ckeditor5-core/src/editor/editor.ts @@ -725,20 +725,15 @@ export default abstract class Editor extends ObservableMixin() { } } - const licenseType: 'trial' | 'development' = licensePayload.licenseType; - if ( licensePayload.licenseType === 'trial' && licensePayload.exp * 1000 < Date.now() ) { blockEditor( this, 'trialLimit' ); - console.info( - `You are using the ${ licenseType } version of CKEditor 5 with limited usage. ` + - 'Make sure you will not use it in the production environment.' - ); - return; } if ( licensePayload.licenseType === 'trial' || licensePayload.licenseType === 'development' ) { + const licenseType: 'trial' | 'development' = licensePayload.licenseType; + console.info( `You are using the ${ licenseType } version of CKEditor 5 with limited usage. ` + 'Make sure you will not use it in the production environment.' diff --git a/packages/ckeditor5-core/tests/editor/licensecheck.js b/packages/ckeditor5-core/tests/editor/licensecheck.js index 68df6fee933..0f59cfe11fd 100644 --- a/packages/ckeditor5-core/tests/editor/licensecheck.js +++ b/packages/ckeditor5-core/tests/editor/licensecheck.js @@ -149,9 +149,6 @@ describe( 'License check', () => { sinon.assert.calledWithMatch( showErrorStub, 'trialLimit' ); expect( editor.isReadOnly ).to.be.true; - sinon.assert.calledOnce( consoleInfoSpy ); - sinon.assert.calledWith( consoleInfoSpy, 'You are using the trial version of CKEditor 5 with ' + - 'limited usage. Make sure you will not use it in the production environment.' ); dateNow.restore(); } ); From aca2c6247a0645fa2b4fe4ef580fed0a24c73aa3 Mon Sep 17 00:00:00 2001 From: Arkadiusz Filipczak Date: Thu, 23 May 2024 10:50:43 +0200 Subject: [PATCH 044/256] License v3 - Check distribution channel. --- packages/ckeditor5-core/src/editor/editor.ts | 23 ++- .../tests/editor/licensecheck.js | 176 ++++++++++++++---- 2 files changed, 165 insertions(+), 34 deletions(-) diff --git a/packages/ckeditor5-core/src/editor/editor.ts b/packages/ckeditor5-core/src/editor/editor.ts index b358be84acb..628d6c4a9b3 100644 --- a/packages/ckeditor5-core/src/editor/editor.ts +++ b/packages/ckeditor5-core/src/editor/editor.ts @@ -14,6 +14,7 @@ import { logError, parseBase64EncodedObject, releaseDate, + toArray, uid, crc32, type Locale, @@ -665,9 +666,15 @@ export default abstract class Editor extends /* #__PURE__ */ ObservableMixin() { */ private _verifyLicenseKey() { const licenseKey = this.config.get( 'licenseKey' ); + const distributionChannel = ( window as any )[ ' CKE_DISTRIBUTION' ] || 'sh'; if ( !licenseKey ) { // TODO: For now, we don't block the editor if a licence key is not provided. GPL is assumed. + + if ( distributionChannel == 'cloud' ) { + blockEditor( this, 'distributionChannel' ); + } + return; } @@ -693,6 +700,12 @@ export default abstract class Editor extends /* #__PURE__ */ ObservableMixin() { return; } + if ( licensePayload.distributionChannel && !toArray( licensePayload.distributionChannel ).includes( distributionChannel ) ) { + blockEditor( this, 'distributionChannel' ); + + return; + } + if ( crc32( getCrcInputData( licensePayload ) ) != licensePayload.vc.toLowerCase() ) { blockEditor( this, 'invalid' ); @@ -855,7 +868,15 @@ export default abstract class Editor extends /* #__PURE__ */ ObservableMixin() { } } -type LicenseErrorReason = 'invalid' | 'expired' | 'domainLimit' | 'featureNotAllowed' | 'trialLimit' | 'developmentLimit' | 'usageLimit'; +type LicenseErrorReason = + 'invalid' | + 'expired' | + 'domainLimit' | + 'featureNotAllowed' | + 'trialLimit' | + 'developmentLimit' | + 'usageLimit' | + 'distributionChannel'; /** * Fired when the {@link module:engine/controller/datacontroller~DataController#event:ready data} and all additional diff --git a/packages/ckeditor5-core/tests/editor/licensecheck.js b/packages/ckeditor5-core/tests/editor/licensecheck.js index 0f59cfe11fd..80eabc7eeff 100644 --- a/packages/ckeditor5-core/tests/editor/licensecheck.js +++ b/packages/ckeditor5-core/tests/editor/licensecheck.js @@ -25,7 +25,7 @@ class TestEditor extends Editor { } } -describe( 'License check', () => { +describe( 'Editor - license check', () => { afterEach( () => { delete TestEditor.builtinPlugins; delete TestEditor.defaultConfig; @@ -79,7 +79,7 @@ describe( 'License check', () => { } ); describe( 'domain check', () => { - it( 'should pass when localhost is in the licensedHosts list', () => { + it( 'should not block if localhost is in the licensedHosts list', () => { const { licenseKey } = generateKey( { licensedHosts: [ 'localhost' ] } ); const editor = new TestEditor( { licenseKey } ); @@ -88,7 +88,7 @@ describe( 'License check', () => { expect( editor.isReadOnly ).to.be.false; } ); - it( 'should not pass when domain is not in the licensedHosts list', () => { + it( 'should block if domain is not in the licensedHosts list', () => { const { licenseKey } = generateKey( { licensedHosts: [ 'facebook.com' ] } ); const editor = new TestEditor( { licenseKey } ); @@ -97,7 +97,7 @@ describe( 'License check', () => { expect( editor.isReadOnly ).to.be.true; } ); - it( 'should not pass if domain have no subdomain', () => { + it( 'should block if domain have no subdomain', () => { const { licenseKey } = generateKey( { licensedHosts: [ '*.localhost' ] } ); const editor = new TestEditor( { licenseKey } ); @@ -107,6 +107,109 @@ describe( 'License check', () => { } ); } ); + describe( 'distribution channel check', () => { + afterEach( () => { + delete window[ ' CKE_DISTRIBUTION' ]; + } ); + + it( 'should not block if distribution channel match', () => { + setChannel( 'xyz' ); + + const { licenseKey } = generateKey( { distributionChannel: 'xyz' } ); + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.notCalled( showErrorStub ); + expect( editor.isReadOnly ).to.be.false; + } ); + + it( 'should not block if one of distribution channel match', () => { + setChannel( 'xyz' ); + + const { licenseKey } = generateKey( { distributionChannel: [ 'abc', 'xyz' ] } ); + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.notCalled( showErrorStub ); + expect( editor.isReadOnly ).to.be.false; + } ); + + it( 'should not block if implicit distribution channel match', () => { + const { licenseKey } = generateKey( { distributionChannel: 'sh' } ); + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.notCalled( showErrorStub ); + expect( editor.isReadOnly ).to.be.false; + } ); + + it( 'should not block if distribution channel is not restricted', () => { + setChannel( 'xyz' ); + + const { licenseKey } = generateKey(); + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.notCalled( showErrorStub ); + expect( editor.isReadOnly ).to.be.false; + } ); + + it( 'should block if distribution channel doesn\'t match', () => { + setChannel( 'abc' ); + + const { licenseKey } = generateKey( { distributionChannel: 'xyz' } ); + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.calledWithMatch( showErrorStub, 'distributionChannel' ); + expect( editor.isReadOnly ).to.be.true; + } ); + + it( 'should block if none of distribution channel doesn\'t match', () => { + setChannel( 'abc' ); + + const { licenseKey } = generateKey( { distributionChannel: [ 'xyz', 'def' ] } ); + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.calledWithMatch( showErrorStub, 'distributionChannel' ); + expect( editor.isReadOnly ).to.be.true; + } ); + + it( 'should block if implicit distribution channel doesn\'t match', () => { + const { licenseKey } = generateKey( { distributionChannel: 'xyz' } ); + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.calledWithMatch( showErrorStub, 'distributionChannel' ); + expect( editor.isReadOnly ).to.be.true; + } ); + + describe( 'GPL license', () => { + it( 'should block if disctribution channel is cloud', () => { + setChannel( 'cloud' ); + + const editor = new TestEditor( {} ); + + sinon.assert.calledWithMatch( showErrorStub, 'distributionChannel' ); + expect( editor.isReadOnly ).to.be.true; + } ); + + it( 'should not block if disctribution channel is not cloud', () => { + setChannel( 'xyz' ); + + const editor = new TestEditor( {} ); + + sinon.assert.notCalled( showErrorStub ); + expect( editor.isReadOnly ).to.be.false; + } ); + } ); + + function setChannel( channel ) { + window[ ' CKE_DISTRIBUTION' ] = channel; + } + } ); + describe( 'trial check', () => { let consoleInfoSpy; @@ -457,19 +560,18 @@ function wait( time ) { } ); } -function generateKey( { - isExpired = false, - jtiExist = true, - expExist = true, - vcExist = true, - customVc = undefined, - skipHeader, - skipTail, - daysAfterExpiration = 0, - licensedHosts, - licenseType, - usageEndpoint -} = {} ) { +function generateKey( options = {} ) { + const { + isExpired = false, + jtiExist = true, + expExist = true, + vcExist = true, + customVc = undefined, + skipHeader = false, + skipTail = false, + daysAfterExpiration = 0 + } = options; + const jti = 'foo'; const releaseTimestamp = Date.parse( releaseDate ); const day = 86400000; // one day in milliseconds. @@ -478,25 +580,33 @@ function generateKey( { // before or after release day. const expirationTimestamp = isExpired ? releaseTimestamp - 10 * day : releaseTimestamp + 10 * day; const todayTimestamp = ( expirationTimestamp + daysAfterExpiration * day ); - const vc = crc32( getCrcInputData( { - jti, - exp: expirationTimestamp / 1000, - licensedHosts, - licenseType, - usageEndpoint - } ) ); - - const payload = encodePayload( { - jti: jtiExist && jti, - vc: customVc || ( vcExist ? vc : undefined ), - exp: expExist && expirationTimestamp / 1000, - licensedHosts, - licenseType, - usageEndpoint + + const payload = {}; + + [ 'licensedHosts', 'licenseType', 'usageEndpoint', 'distributionChannel' ].forEach( prop => { + if ( prop in options ) { + payload[ prop ] = options[ prop ]; + } } ); + if ( jtiExist ) { + payload.jti = jti; + } + + if ( expExist ) { + payload.exp = Math.ceil( expirationTimestamp / 1000 ); + } + + if ( customVc ) { + payload.vc = customVc; + } else if ( vcExist ) { + const vc = crc32( getCrcInputData( payload ) ); + + payload.vc = vc; + } + return { - licenseKey: `${ skipHeader ? '' : 'foo.' }${ payload }${ skipTail ? '' : '.bar' }`, + licenseKey: `${ skipHeader ? '' : 'foo.' }${ encodePayload( payload ) }${ skipTail ? '' : '.bar' }`, todayTimestamp }; } From 4617f0ab5fed77e9ad6a25d06016617c334e9d24 Mon Sep 17 00:00:00 2001 From: Marta Motyczynska Date: Sun, 26 May 2024 23:49:20 +0200 Subject: [PATCH 045/256] Handle GPL license. --- packages/ckeditor5-core/src/editor/editor.ts | 7 ++++- .../tests/editor/licensecheck.js | 28 ++++++++++++++++--- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/packages/ckeditor5-core/src/editor/editor.ts b/packages/ckeditor5-core/src/editor/editor.ts index 628d6c4a9b3..b2907b2656e 100644 --- a/packages/ckeditor5-core/src/editor/editor.ts +++ b/packages/ckeditor5-core/src/editor/editor.ts @@ -669,8 +669,12 @@ export default abstract class Editor extends /* #__PURE__ */ ObservableMixin() { const distributionChannel = ( window as any )[ ' CKE_DISTRIBUTION' ] || 'sh'; if ( !licenseKey ) { - // TODO: For now, we don't block the editor if a licence key is not provided. GPL is assumed. + blockEditor( this, 'noLicense' ); + return; + } + + if ( licenseKey == 'GPL' ) { if ( distributionChannel == 'cloud' ) { blockEditor( this, 'distributionChannel' ); } @@ -869,6 +873,7 @@ export default abstract class Editor extends /* #__PURE__ */ ObservableMixin() { } type LicenseErrorReason = + 'noLicense' | 'invalid' | 'expired' | 'domainLimit' | diff --git a/packages/ckeditor5-core/tests/editor/licensecheck.js b/packages/ckeditor5-core/tests/editor/licensecheck.js index 80eabc7eeff..7573ac0d643 100644 --- a/packages/ckeditor5-core/tests/editor/licensecheck.js +++ b/packages/ckeditor5-core/tests/editor/licensecheck.js @@ -186,19 +186,21 @@ describe( 'Editor - license check', () => { } ); describe( 'GPL license', () => { - it( 'should block if disctribution channel is cloud', () => { + it( 'should block if distribution channel is cloud', () => { setChannel( 'cloud' ); - const editor = new TestEditor( {} ); + const licenseKey = 'GPL'; + const editor = new TestEditor( { licenseKey } ); sinon.assert.calledWithMatch( showErrorStub, 'distributionChannel' ); expect( editor.isReadOnly ).to.be.true; } ); - it( 'should not block if disctribution channel is not cloud', () => { + it( 'should not block if distribution channel is not cloud', () => { setChannel( 'xyz' ); - const editor = new TestEditor( {} ); + const licenseKey = 'GPL'; + const editor = new TestEditor( { licenseKey } ); sinon.assert.notCalled( showErrorStub ); expect( editor.isReadOnly ).to.be.false; @@ -210,6 +212,24 @@ describe( 'Editor - license check', () => { } } ); + describe( 'GPL check', () => { + it( 'should not block if license key is GPL', () => { + const licenseKey = 'GPL'; + + const editor = new TestEditor( { licenseKey } ); + + sinon.assert.notCalled( showErrorStub ); + expect( editor.isReadOnly ).to.be.false; + } ); + + it( 'should block if license key is missing', () => { + const editor = new TestEditor( {} ); + + sinon.assert.calledWithMatch( showErrorStub, 'noLicense' ); + expect( editor.isReadOnly ).to.be.true; + } ); + } ); + describe( 'trial check', () => { let consoleInfoSpy; From 1fe0dc5cfe601adf4691ca6b69d98cf1f5170855 Mon Sep 17 00:00:00 2001 From: Marta Motyczynska Date: Sun, 26 May 2024 23:50:09 +0200 Subject: [PATCH 046/256] Show powerdby for gpl license. --- packages/ckeditor5-ui/src/editorui/poweredby.ts | 3 ++- packages/ckeditor5-ui/tests/editorui/poweredby.js | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5-ui/src/editorui/poweredby.ts b/packages/ckeditor5-ui/src/editorui/poweredby.ts index 411d48601b4..6c8f69216da 100644 --- a/packages/ckeditor5-ui/src/editorui/poweredby.ts +++ b/packages/ckeditor5-ui/src/editorui/poweredby.ts @@ -104,7 +104,8 @@ export default class PoweredBy extends /* #__PURE__ */ DomEmitterMixin() { if ( !forceVisible ) { const licenseKey = editor.config.get( 'licenseKey' ); - const licenseContent = licenseKey && parseBase64EncodedObject( licenseKey.split( '.' )[ 1 ] ); + const isGpl = licenseKey && licenseKey == 'GPL'; + const licenseContent = licenseKey && !isGpl && parseBase64EncodedObject( licenseKey.split( '.' )[ 1 ] ); if ( licenseContent && licenseContent.whiteLabel ) { return; diff --git a/packages/ckeditor5-ui/tests/editorui/poweredby.js b/packages/ckeditor5-ui/tests/editorui/poweredby.js index 56d5a60d5c4..513b84e9136 100644 --- a/packages/ckeditor5-ui/tests/editorui/poweredby.js +++ b/packages/ckeditor5-ui/tests/editorui/poweredby.js @@ -83,6 +83,20 @@ describe( 'PoweredBy', () => { expect( editor.ui.poweredBy._balloonView ).to.be.instanceOf( BalloonPanelView ); } ); + it( 'should create the balloon when license is `GPL`', async () => { + const editor = await createEditor( element, { + licenseKey: 'GPL' + } ); + + expect( editor.ui.poweredBy._balloonView ).to.be.null; + + focusEditor( editor ); + + expect( editor.ui.poweredBy._balloonView ).to.be.instanceOf( BalloonPanelView ); + + await editor.destroy(); + } ); + it( 'should not create the balloon when a white-label license key is configured', async () => { const editor = await createEditor( element, { licenseKey: 'foo.eyJ3aGl0ZUxhYmVsIjp0cnVlLCJleHAiOjIyMDg5ODg4MDB9.bar' From 55018b97675cacc5e5713e494266ccc577df842b Mon Sep 17 00:00:00 2001 From: Marta Motyczynska Date: Mon, 27 May 2024 21:47:08 +0200 Subject: [PATCH 047/256] Add GPL as default license key if it's missing in config (only our testing environment). --- packages/ckeditor5-core/src/editor/editor.ts | 11 ++++++++++- .../ckeditor5-core/tests/editor/licensecheck.js | 13 +++++++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/packages/ckeditor5-core/src/editor/editor.ts b/packages/ckeditor5-core/src/editor/editor.ts index b2907b2656e..29ed698ee14 100644 --- a/packages/ckeditor5-core/src/editor/editor.ts +++ b/packages/ckeditor5-core/src/editor/editor.ts @@ -42,6 +42,11 @@ import Accessibility from '../accessibility.js'; import type { LoadedPlugins, PluginConstructor } from '../plugin.js'; import type { EditorConfig } from './editorconfig.js'; +declare global { + // eslint-disable-next-line no-var + var CKEDITOR_IS_TEST_ENV: string; +} + /** * The class representing a basic, generic editor. * @@ -665,9 +670,13 @@ export default abstract class Editor extends /* #__PURE__ */ ObservableMixin() { * @internal */ private _verifyLicenseKey() { - const licenseKey = this.config.get( 'licenseKey' ); + let licenseKey = this.config.get( 'licenseKey' ); const distributionChannel = ( window as any )[ ' CKE_DISTRIBUTION' ] || 'sh'; + if ( !licenseKey && window.CKEDITOR_IS_TEST_ENV ) { + this.config.set( 'licenseKey', licenseKey = 'GPL' ); + } + if ( !licenseKey ) { blockEditor( this, 'noLicense' ); diff --git a/packages/ckeditor5-core/tests/editor/licensecheck.js b/packages/ckeditor5-core/tests/editor/licensecheck.js index 7573ac0d643..3db0109cfd0 100644 --- a/packages/ckeditor5-core/tests/editor/licensecheck.js +++ b/packages/ckeditor5-core/tests/editor/licensecheck.js @@ -215,18 +215,27 @@ describe( 'Editor - license check', () => { describe( 'GPL check', () => { it( 'should not block if license key is GPL', () => { const licenseKey = 'GPL'; - const editor = new TestEditor( { licenseKey } ); sinon.assert.notCalled( showErrorStub ); expect( editor.isReadOnly ).to.be.false; } ); - it( 'should block if license key is missing', () => { + it( 'should not block if license key is missing (CKEditor testing environment)', () => { + const editor = new TestEditor( {} ); + + sinon.assert.notCalled( showErrorStub ); + expect( editor.isReadOnly ).to.be.false; + } ); + + it( 'should block if license key is missing (outside of CKEditor testing environment)', () => { + window.CKEDITOR_IS_TEST_ENV = undefined; const editor = new TestEditor( {} ); sinon.assert.calledWithMatch( showErrorStub, 'noLicense' ); expect( editor.isReadOnly ).to.be.true; + + window.CKEDITOR_IS_TEST_ENV = true; } ); } ); From bb5d8c5042a635da3480338652ea4341199d2f3b Mon Sep 17 00:00:00 2001 From: Arkadiusz Filipczak Date: Wed, 29 May 2024 15:44:39 +0200 Subject: [PATCH 048/256] License v3 - paste-from-office tests should accept async config. --- .../tests/_utils/utils.js | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/packages/ckeditor5-paste-from-office/tests/_utils/utils.js b/packages/ckeditor5-paste-from-office/tests/_utils/utils.js index c0383d3f2ca..8b1576adec0 100644 --- a/packages/ckeditor5-paste-from-office/tests/_utils/utils.js +++ b/packages/ckeditor5-paste-from-office/tests/_utils/utils.js @@ -81,7 +81,9 @@ export function generateTests( config ) { describe( config.type, () => { describe( config.input, () => { - const editorConfig = config.editorConfig || {}; + const editorConfig = typeof config.editorConfig == 'function' ? + config.editorConfig : + () => Promise.resolve( config.editorConfig || {} ); for ( const group of Object.keys( groups ) ) { const skip = config.skip && config.skip[ group ] || []; @@ -152,12 +154,8 @@ function generateNormalizationTests( title, fixtures, editorConfig, skip, only ) describe( title, () => { let editor; - beforeEach( () => { - return VirtualTestEditor - .create( editorConfig ) - .then( newEditor => { - editor = newEditor; - } ); + beforeEach( async () => { + editor = await VirtualTestEditor.create( await editorConfig() ); } ); afterEach( () => { @@ -207,16 +205,12 @@ function generateIntegrationTests( title, fixtures, editorConfig, skip, only ) { let element, editor; let data = {}; - before( () => { + before( async () => { element = document.createElement( 'div' ); document.body.appendChild( element ); - return ClassicTestEditor - .create( element, editorConfig ) - .then( editorInstance => { - editor = editorInstance; - } ); + editor = await ClassicTestEditor.create( element, await editorConfig() ); } ); beforeEach( () => { From 3d08cf61d38be176c7e51b4049bdbd0856fd9ab0 Mon Sep 17 00:00:00 2001 From: Marta Motyczynska Date: Thu, 6 Jun 2024 14:05:53 +0200 Subject: [PATCH 049/256] Use global variable to pass license key (if missing in config). --- packages/ckeditor5-core/src/editor/editor.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ckeditor5-core/src/editor/editor.ts b/packages/ckeditor5-core/src/editor/editor.ts index 29ed698ee14..6b80076b2ec 100644 --- a/packages/ckeditor5-core/src/editor/editor.ts +++ b/packages/ckeditor5-core/src/editor/editor.ts @@ -44,7 +44,7 @@ import type { EditorConfig } from './editorconfig.js'; declare global { // eslint-disable-next-line no-var - var CKEDITOR_IS_TEST_ENV: string; + var CKEDITOR_GLOBAL_LICENSE_KEY: string; } /** @@ -673,8 +673,8 @@ export default abstract class Editor extends /* #__PURE__ */ ObservableMixin() { let licenseKey = this.config.get( 'licenseKey' ); const distributionChannel = ( window as any )[ ' CKE_DISTRIBUTION' ] || 'sh'; - if ( !licenseKey && window.CKEDITOR_IS_TEST_ENV ) { - this.config.set( 'licenseKey', licenseKey = 'GPL' ); + if ( !licenseKey && window.CKEDITOR_GLOBAL_LICENSE_KEY ) { + this.config.set( 'licenseKey', licenseKey = window.CKEDITOR_GLOBAL_LICENSE_KEY ); } if ( !licenseKey ) { From 5392aacbc7b0f9c97f4b557309d49cac052ef310 Mon Sep 17 00:00:00 2001 From: Marta Motyczynska Date: Thu, 6 Jun 2024 14:21:35 +0200 Subject: [PATCH 050/256] Update tests with new variable name and value. --- packages/ckeditor5-core/tests/editor/licensecheck.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ckeditor5-core/tests/editor/licensecheck.js b/packages/ckeditor5-core/tests/editor/licensecheck.js index 3db0109cfd0..ca7f7bc08d0 100644 --- a/packages/ckeditor5-core/tests/editor/licensecheck.js +++ b/packages/ckeditor5-core/tests/editor/licensecheck.js @@ -229,13 +229,13 @@ describe( 'Editor - license check', () => { } ); it( 'should block if license key is missing (outside of CKEditor testing environment)', () => { - window.CKEDITOR_IS_TEST_ENV = undefined; + window.CKEDITOR_GLOBAL_LICENSE_KEY = undefined; const editor = new TestEditor( {} ); sinon.assert.calledWithMatch( showErrorStub, 'noLicense' ); expect( editor.isReadOnly ).to.be.true; - window.CKEDITOR_IS_TEST_ENV = true; + window.CKEDITOR_GLOBAL_LICENSE_KEY = 'GPL'; } ); } ); From e898e83b4599cae99c4a7588b0e8b058f73afad0 Mon Sep 17 00:00:00 2001 From: Marta Motyczynska Date: Wed, 12 Jun 2024 12:17:54 +0200 Subject: [PATCH 051/256] Throw error when license key is missing (instead of blocking the editor). --- packages/ckeditor5-core/src/editor/editor.ts | 33 ++++++++++++------- .../tests/editor/licensecheck.js | 29 ++++++++-------- 2 files changed, 37 insertions(+), 25 deletions(-) diff --git a/packages/ckeditor5-core/src/editor/editor.ts b/packages/ckeditor5-core/src/editor/editor.ts index 6b80076b2ec..4bb4cc29f13 100644 --- a/packages/ckeditor5-core/src/editor/editor.ts +++ b/packages/ckeditor5-core/src/editor/editor.ts @@ -312,6 +312,8 @@ export default abstract class Editor extends /* #__PURE__ */ ObservableMixin() { this.config.define( 'plugins', availablePlugins ); this.config.define( this._context._getEditorConfig() ); + checkLicenseKeyIsDefined( this.config ); + this.plugins = new PluginCollection( this, availablePlugins, this._context.plugins ); this.locale = this._context.locale; @@ -348,6 +350,24 @@ export default abstract class Editor extends /* #__PURE__ */ ObservableMixin() { this.keystrokes.listenTo( this.editing.view.document ); this.accessibility = new Accessibility( this ); + + // Checks if the license key is defined and throws an error if it is not. + function checkLicenseKeyIsDefined( config: Config ) { + let licenseKey = config.get( 'licenseKey' ); + + if ( !licenseKey && window.CKEDITOR_GLOBAL_LICENSE_KEY ) { + config.set( 'licenseKey', licenseKey = window.CKEDITOR_GLOBAL_LICENSE_KEY ); + } + + if ( !licenseKey ) { + /** + * The license key is missing. + * + * @error editor-license-key-missing + */ + throw new CKEditorError( 'editor-license-key-missing' ); + } + } } /** @@ -670,19 +690,9 @@ export default abstract class Editor extends /* #__PURE__ */ ObservableMixin() { * @internal */ private _verifyLicenseKey() { - let licenseKey = this.config.get( 'licenseKey' ); + const licenseKey = this.config.get( 'licenseKey' )!; const distributionChannel = ( window as any )[ ' CKE_DISTRIBUTION' ] || 'sh'; - if ( !licenseKey && window.CKEDITOR_GLOBAL_LICENSE_KEY ) { - this.config.set( 'licenseKey', licenseKey = window.CKEDITOR_GLOBAL_LICENSE_KEY ); - } - - if ( !licenseKey ) { - blockEditor( this, 'noLicense' ); - - return; - } - if ( licenseKey == 'GPL' ) { if ( distributionChannel == 'cloud' ) { blockEditor( this, 'distributionChannel' ); @@ -882,7 +892,6 @@ export default abstract class Editor extends /* #__PURE__ */ ObservableMixin() { } type LicenseErrorReason = - 'noLicense' | 'invalid' | 'expired' | 'domainLimit' | diff --git a/packages/ckeditor5-core/tests/editor/licensecheck.js b/packages/ckeditor5-core/tests/editor/licensecheck.js index ca7f7bc08d0..b8142d357b6 100644 --- a/packages/ckeditor5-core/tests/editor/licensecheck.js +++ b/packages/ckeditor5-core/tests/editor/licensecheck.js @@ -6,6 +6,7 @@ /* globals window, console, Response, globalThis, btoa */ import { releaseDate, crc32 } from '@ckeditor/ckeditor5-utils'; +import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror.js'; import Editor from '../../src/editor/editor.js'; import testUtils from '../../tests/_utils/utils.js'; @@ -213,27 +214,29 @@ describe( 'Editor - license check', () => { } ); describe( 'GPL check', () => { - it( 'should not block if license key is GPL', () => { + it( 'should not throw if license key is GPL', () => { const licenseKey = 'GPL'; - const editor = new TestEditor( { licenseKey } ); - sinon.assert.notCalled( showErrorStub ); - expect( editor.isReadOnly ).to.be.false; + expect( () => { + // eslint-disable-next-line no-new + new TestEditor( { licenseKey } ); + } ).to.not.throw(); } ); - it( 'should not block if license key is missing (CKEditor testing environment)', () => { - const editor = new TestEditor( {} ); - - sinon.assert.notCalled( showErrorStub ); - expect( editor.isReadOnly ).to.be.false; + it( 'should not throw if license key is missing (CKEditor testing environment)', () => { + expect( () => { + // eslint-disable-next-line no-new + new TestEditor( {} ); + } ).to.not.throw(); } ); - it( 'should block if license key is missing (outside of CKEditor testing environment)', () => { + it( 'should throw if license key is missing (outside of CKEditor testing environment)', () => { window.CKEDITOR_GLOBAL_LICENSE_KEY = undefined; - const editor = new TestEditor( {} ); - sinon.assert.calledWithMatch( showErrorStub, 'noLicense' ); - expect( editor.isReadOnly ).to.be.true; + expect( () => { + // eslint-disable-next-line no-new + new TestEditor( {} ); + } ).to.throw( CKEditorError, 'editor-license-key-missing' ); window.CKEDITOR_GLOBAL_LICENSE_KEY = 'GPL'; } ); From 746687cf212d81da3962aab97c74571c0d2824d8 Mon Sep 17 00:00:00 2001 From: Marta Motyczynska Date: Wed, 12 Jun 2024 12:29:03 +0200 Subject: [PATCH 052/256] Refactor GPL handling in poweredby. --- packages/ckeditor5-ui/src/editorui/poweredby.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/ckeditor5-ui/src/editorui/poweredby.ts b/packages/ckeditor5-ui/src/editorui/poweredby.ts index 6c8f69216da..fcb41f41162 100644 --- a/packages/ckeditor5-ui/src/editorui/poweredby.ts +++ b/packages/ckeditor5-ui/src/editorui/poweredby.ts @@ -103,9 +103,8 @@ export default class PoweredBy extends /* #__PURE__ */ DomEmitterMixin() { const forceVisible = editor.config.get( 'ui.poweredBy.forceVisible' ); if ( !forceVisible ) { - const licenseKey = editor.config.get( 'licenseKey' ); - const isGpl = licenseKey && licenseKey == 'GPL'; - const licenseContent = licenseKey && !isGpl && parseBase64EncodedObject( licenseKey.split( '.' )[ 1 ] ); + const licenseKey = editor.config.get( 'licenseKey' )!; + const licenseContent = licenseKey != 'GPL' && parseBase64EncodedObject( licenseKey.split( '.' )[ 1 ] ); if ( licenseContent && licenseContent.whiteLabel ) { return; From d0a30cf1ef21869abe88c2f0c21369644256d43a Mon Sep 17 00:00:00 2001 From: Arkadiusz Filipczak Date: Thu, 13 Jun 2024 12:24:50 +0200 Subject: [PATCH 053/256] Throw license-related errors. --- packages/ckeditor5-core/src/editor/editor.ts | 96 +++++++++++++++++-- .../tests/editor/licensecheck.js | 49 ++++++++++ 2 files changed, 136 insertions(+), 9 deletions(-) diff --git a/packages/ckeditor5-core/src/editor/editor.ts b/packages/ckeditor5-core/src/editor/editor.ts index 4bb4cc29f13..aa3dae87e1d 100644 --- a/packages/ckeditor5-core/src/editor/editor.ts +++ b/packages/ckeditor5-core/src/editor/editor.ts @@ -857,17 +857,95 @@ export default abstract class Editor extends /* #__PURE__ */ ObservableMixin() { /** * @internal */ - /* istanbul ignore next -- @preserve */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - private _showLicenseError( reason: LicenseErrorReason, featureName?: string ) { + private _showLicenseError( reason: LicenseErrorReason, pluginName?: string ) { // Make sure the error thrown is unhandled. setTimeout( () => { - /** - * TODO: consider error kinds for each reason. - * - * @error todo-specify-this-error-code - */ - throw new CKEditorError( 'todo-specify-this-error-code', null ); + if ( reason == 'invalid' ) { + /** + * Invalid license key. Please contact our customer support at https://ckeditor.com/contact/. + * + * @error invalid-license-key + */ + throw new CKEditorError( 'invalid-license-key', this ); + } + + if ( reason == 'expired' ) { + /** + * Your license key has expired. Please renew your license at https://ckeditor.com/TODO/. + * + * @error license-key-expired + */ + throw new CKEditorError( 'license-key-expired', this ); + } + + if ( reason == 'domainLimit' ) { + /** + * The hostname is not allowed by your license. Please update your license configuration at https://ckeditor.com/TODO/. + * + * @error license-key-domain-limit + */ + throw new CKEditorError( 'license-key-domain-limit', this ); + } + + if ( reason == 'featureNotAllowed' ) { + /** + * The plugin is not allowed by your license. + * + * Please check your license or contact support at https://ckeditor.com/contact/ for more information. + * + * @error license-key-feature-not-allowed + * @param {String} pluginName + */ + throw new CKEditorError( 'license-key-feature-not-allowed', this, { pluginName } ); + } + + if ( reason == 'trialLimit' ) { + /** + * You have exhausted the trial usage limit. Restart the editor. + * + * Please contact our customer support to get full access at https://ckeditor.com/contact/. + * + * @error license-key-trial-limit + */ + throw new CKEditorError( 'license-key-trial-limit', this ); + } + + if ( reason == 'developmentLimit' ) { + /** + * You have reached the development usage limit. Restart the editor. + * + * Please contact our customer support to get full access at https://ckeditor.com/contact/. + * + * @error license-key-development-limit + */ + throw new CKEditorError( 'license-key-development-limit', this ); + } + + if ( reason == 'usageLimit' ) { + /** + * The editor usage limit has been reached. + * + * Visit Contact support to extend the limit at https://ckeditor.com/contact/. + * + * @error license-key-usage-limit + */ + throw new CKEditorError( 'license-key-usage-limit', this ); + } + + if ( reason == 'distributionChannel' ) { + /** + * The usage is not valid for this distribution channel. + * + * Please check your installation or contact support at https://ckeditor.com/contact/ for more information. + * + * @error license-key-distribution-channel + */ + throw new CKEditorError( 'license-key-distribution-channel', this ); + } + + /* istanbul ignore next -- @preserve */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const unreachable: never = reason; }, 0 ); this._showLicenseError = () => {}; diff --git a/packages/ckeditor5-core/tests/editor/licensecheck.js b/packages/ckeditor5-core/tests/editor/licensecheck.js index b8142d357b6..4e4d4c86ea7 100644 --- a/packages/ckeditor5-core/tests/editor/licensecheck.js +++ b/packages/ckeditor5-core/tests/editor/licensecheck.js @@ -7,6 +7,7 @@ import { releaseDate, crc32 } from '@ckeditor/ckeditor5-utils'; import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror.js'; +import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils.js'; import Editor from '../../src/editor/editor.js'; import testUtils from '../../tests/_utils/utils.js'; @@ -584,6 +585,54 @@ describe( 'Editor - license check', () => { expect( editor.isReadOnly ).to.be.true; } ); } ); + + describe( 'license errors', () => { + let clock; + + beforeEach( () => { + clock = sinon.useFakeTimers( { toFake: [ 'setTimeout' ] } ); + } ); + + const testCases = [ + { reason: 'invalid', error: 'invalid-license-key' }, + { reason: 'expired', error: 'license-key-expired' }, + { reason: 'domainLimit', error: 'license-key-domain-limit' }, + { reason: 'featureNotAllowed', error: 'license-key-feature-not-allowed', pluginName: 'PluginABC' }, + { reason: 'trialLimit', error: 'license-key-trial-limit' }, + { reason: 'developmentLimit', error: 'license-key-development-limit' }, + { reason: 'usageLimit', error: 'license-key-usage-limit' }, + { reason: 'distributionChannel', error: 'license-key-distribution-channel' } + ]; + + for ( const testCase of testCases ) { + const { reason, error, pluginName } = testCase; + const expectedData = pluginName ? { pluginName } : undefined; + + it( `should throw \`${ error }\` error`, () => { + const editor = new TestEditor( { licenseKey: 'GPL' } ); + + editor._showLicenseError( reason, pluginName ); + + expectToThrowCKEditorError( () => clock.tick( 1 ), error, editor, expectedData ); + } ); + } + + it( 'should throw error only once', () => { + const editor = new TestEditor( { licenseKey: 'GPL' } ); + + editor._showLicenseError( 'invalid' ); + + try { + clock.tick( 1 ); + } catch ( e ) { + // Do nothing. + } + + editor._showLicenseError( 'invalid' ); + + expect( () => clock.tick( 1 ) ).to.not.throw(); + } ); + } ); } ); function wait( time ) { From 191fff97b2c0c664f2fe9e12295616f66c11b0e0 Mon Sep 17 00:00:00 2001 From: Arkadiusz Filipczak Date: Thu, 13 Jun 2024 14:47:40 +0200 Subject: [PATCH 054/256] Moved license verification. --- packages/ckeditor5-core/src/editor/editor.ts | 339 +++++++++---------- 1 file changed, 166 insertions(+), 173 deletions(-) diff --git a/packages/ckeditor5-core/src/editor/editor.ts b/packages/ckeditor5-core/src/editor/editor.ts index aa3dae87e1d..664a5952974 100644 --- a/packages/ckeditor5-core/src/editor/editor.ts +++ b/packages/ckeditor5-core/src/editor/editor.ts @@ -344,13 +344,13 @@ export default abstract class Editor extends /* #__PURE__ */ ObservableMixin() { this.conversion.addAlias( 'dataDowncast', this.data.downcastDispatcher ); this.conversion.addAlias( 'editingDowncast', this.editing.downcastDispatcher ); - this._verifyLicenseKey(); - this.keystrokes = new EditingKeystrokeHandler( this ); this.keystrokes.listenTo( this.editing.view.document ); this.accessibility = new Accessibility( this ); + verifyLicenseKey( this ); + // Checks if the license key is defined and throws an error if it is not. function checkLicenseKeyIsDefined( config: Config ) { let licenseKey = config.get( 'licenseKey' ); @@ -368,6 +368,170 @@ export default abstract class Editor extends /* #__PURE__ */ ObservableMixin() { throw new CKEditorError( 'editor-license-key-missing' ); } } + + function verifyLicenseKey( editor: Editor ) { + const licenseKey = editor.config.get( 'licenseKey' )!; + const distributionChannel = ( window as any )[ ' CKE_DISTRIBUTION' ] || 'sh'; + + if ( licenseKey == 'GPL' ) { + if ( distributionChannel == 'cloud' ) { + blockEditor( 'distributionChannel' ); + } + + return; + } + + const encodedPayload = getPayload( licenseKey ); + + if ( !encodedPayload ) { + blockEditor( 'invalid' ); + + return; + } + + const licensePayload = parseBase64EncodedObject( encodedPayload ); + + if ( !licensePayload ) { + blockEditor( 'invalid' ); + + return; + } + + if ( !hasAllRequiredFields( licensePayload ) ) { + blockEditor( 'invalid' ); + + return; + } + + if ( licensePayload.distributionChannel && !toArray( licensePayload.distributionChannel ).includes( distributionChannel ) ) { + blockEditor( 'distributionChannel' ); + + return; + } + + if ( crc32( getCrcInputData( licensePayload ) ) != licensePayload.vc.toLowerCase() ) { + blockEditor( 'invalid' ); + + return; + } + + const expirationDate = new Date( licensePayload.exp * 1000 ); + + if ( expirationDate < releaseDate ) { + blockEditor( 'expired' ); + + return; + } + + const licensedHosts: Array | undefined = licensePayload.licensedHosts; + + if ( licensedHosts ) { + const hostname = window.location.hostname; + const willcards = licensedHosts + .filter( val => val.slice( 0, 1 ) === '*' ) + .map( val => val.slice( 1 ) ); + + const isHostnameMatched = licensedHosts.some( licensedHost => licensedHost === hostname ); + const isWillcardMatched = willcards.some( willcard => willcard === hostname.slice( -willcard.length ) ); + + if ( !isWillcardMatched && !isHostnameMatched ) { + blockEditor( 'domainLimit' ); + + return; + } + } + + if ( licensePayload.licenseType === 'trial' && licensePayload.exp * 1000 < Date.now() ) { + blockEditor( 'trialLimit' ); + + return; + } + + if ( licensePayload.licenseType === 'trial' || licensePayload.licenseType === 'development' ) { + const licenseType: 'trial' | 'development' = licensePayload.licenseType; + + console.info( + `You are using the ${ licenseType } version of CKEditor 5 with limited usage. ` + + 'Make sure you will not use it in the production environment.' + ); + + const timerId = setTimeout( () => { + blockEditor( `${ licenseType }Limit` ); + }, 600000 /* 10 minutes */ ); + + editor.on( 'destroy', () => { + clearTimeout( timerId ); + } ); + } + + if ( licensePayload.usageEndpoint ) { + editor.once( 'ready', () => { + const telemetry = editor._getTelemetryData(); + + const request = { + requestId: uid(), + requestTime: Math.round( Date.now() / 1000 ), + license: licenseKey, + telemetry + }; + + editor._sendUsageRequest( licensePayload.usageEndpoint, request ).then( response => { + const { status, message } = response; + + if ( message ) { + console.warn( message ); + } + + if ( status != 'ok' ) { + blockEditor( 'usageLimit' ); + } + }, () => { + /** + * Your license key cannot be validated because of a network issue. + * Please make sure that your setup does not block the request. + * + * @error license-key-validaton-endpoint-not-reachable + * @param {String} url The URL that was attempted to reach. + */ + logError( 'license-key-validaton-endpoint-not-reachable', { url: licensePayload.usageEndpoint } ); + } ); + }, { priority: 'high' } ); + } + + function getPayload( licenseKey: string ): string | null { + const parts = licenseKey.split( '.' ); + + if ( parts.length != 3 ) { + return null; + } + + return parts[ 1 ]; + } + + function blockEditor( reason: LicenseErrorReason ) { + editor.enableReadOnlyMode( Symbol( 'invalidLicense' ) ); + editor._showLicenseError( reason ); + } + + function hasAllRequiredFields( licensePayload: Record ) { + const requiredFields = [ 'exp', 'jti', 'vc' ]; + + return requiredFields.every( field => field in licensePayload ); + } + + /** + * Returns an array of values that are used to calculate the CRC32 checksum. + */ + function getCrcInputData( licensePayload: Record ): CRCData { + const keysToCheck = Object.getOwnPropertyNames( licensePayload ).sort(); + + const filteredValues = keysToCheck + .filter( key => key != 'vc' && licensePayload[ key ] != null ) + .map( key => licensePayload[ key ] ); + + return [ ...filteredValues ] as CRCData; + } + } } /** @@ -683,177 +847,6 @@ export default abstract class Editor extends /* #__PURE__ */ ObservableMixin() { }; } - /** - * Performs basic license key check. Enables the editor's read-only mode if the license key's validation period has expired - * or the license key format is incorrect. - * - * @internal - */ - private _verifyLicenseKey() { - const licenseKey = this.config.get( 'licenseKey' )!; - const distributionChannel = ( window as any )[ ' CKE_DISTRIBUTION' ] || 'sh'; - - if ( licenseKey == 'GPL' ) { - if ( distributionChannel == 'cloud' ) { - blockEditor( this, 'distributionChannel' ); - } - - return; - } - - const encodedPayload = getPayload( licenseKey ); - - if ( !encodedPayload ) { - blockEditor( this, 'invalid' ); - - return; - } - - const licensePayload = parseBase64EncodedObject( encodedPayload ); - - if ( !licensePayload ) { - blockEditor( this, 'invalid' ); - - return; - } - - if ( !hasAllRequiredFields( licensePayload ) ) { - blockEditor( this, 'invalid' ); - - return; - } - - if ( licensePayload.distributionChannel && !toArray( licensePayload.distributionChannel ).includes( distributionChannel ) ) { - blockEditor( this, 'distributionChannel' ); - - return; - } - - if ( crc32( getCrcInputData( licensePayload ) ) != licensePayload.vc.toLowerCase() ) { - blockEditor( this, 'invalid' ); - - return; - } - - const expirationDate = new Date( licensePayload.exp * 1000 ); - - if ( expirationDate < releaseDate ) { - blockEditor( this, 'expired' ); - - return; - } - - const licensedHosts: Array | undefined = licensePayload.licensedHosts; - - if ( licensedHosts ) { - const hostname = window.location.hostname; - const willcards = licensedHosts - .filter( val => val.slice( 0, 1 ) === '*' ) - .map( val => val.slice( 1 ) ); - - const isHostnameMatched = licensedHosts.some( licensedHost => licensedHost === hostname ); - const isWillcardMatched = willcards.some( willcard => willcard === hostname.slice( -willcard.length ) ); - - if ( !isWillcardMatched && !isHostnameMatched ) { - blockEditor( this, 'domainLimit' ); - - return; - } - } - - if ( licensePayload.licenseType === 'trial' && licensePayload.exp * 1000 < Date.now() ) { - blockEditor( this, 'trialLimit' ); - - return; - } - - if ( licensePayload.licenseType === 'trial' || licensePayload.licenseType === 'development' ) { - const licenseType: 'trial' | 'development' = licensePayload.licenseType; - - console.info( - `You are using the ${ licenseType } version of CKEditor 5 with limited usage. ` + - 'Make sure you will not use it in the production environment.' - ); - - const timerId = setTimeout( () => { - blockEditor( this, `${ licenseType }Limit` ); - }, 600000 /* 10 minutes */ ); - - this.on( 'destroy', () => { - clearTimeout( timerId ); - } ); - } - - if ( licensePayload.usageEndpoint ) { - this.once( 'ready', () => { - const telemetry = this._getTelemetryData(); - - const request = { - requestId: uid(), - requestTime: Math.round( Date.now() / 1000 ), - license: licenseKey, - telemetry - }; - - this._sendUsageRequest( licensePayload.usageEndpoint, request ).then( response => { - const { status, message } = response; - - if ( message ) { - console.warn( message ); - } - - if ( status != 'ok' ) { - blockEditor( this, 'usageLimit' ); - } - }, () => { - /** - * Your license key cannot be validated because of a network issue. - * Please make sure that your setup does not block the request. - * - * @error license-key-validaton-endpoint-not-reachable - * @param {String} url The URL that was attempted to reach. - */ - logError( 'license-key-validaton-endpoint-not-reachable', { url: licensePayload.usageEndpoint } ); - } ); - }, { priority: 'high' } ); - } - - function getPayload( licenseKey: string ): string | null { - const parts = licenseKey.split( '.' ); - - if ( parts.length != 3 ) { - return null; - } - - return parts[ 1 ]; - } - - function blockEditor( editor: Editor, reason: LicenseErrorReason ) { - editor._showLicenseError( reason ); - - editor.enableReadOnlyMode( Symbol( 'invalidLicense' ) ); - } - - function hasAllRequiredFields( licensePayload: Record ) { - const requiredFields = [ 'exp', 'jti', 'vc' ]; - - return requiredFields.every( field => field in licensePayload ); - } - - /** - * Returns an array of values that are used to calculate the CRC32 checksum. - */ - function getCrcInputData( licensePayload: Record ): CRCData { - const keysToCheck = Object.getOwnPropertyNames( licensePayload ).sort(); - - const filteredValues = keysToCheck - .filter( key => key != 'vc' && licensePayload[ key ] != null ) - .map( key => licensePayload[ key ] ); - - return [ ...filteredValues ] as CRCData; - } - } - /** * @internal */ From c9518f413524053f97e379f47f9e9eefb8dbedad Mon Sep 17 00:00:00 2001 From: Arkadiusz Filipczak Date: Tue, 18 Jun 2024 10:17:06 +0200 Subject: [PATCH 055/256] Misc. license fixes after review. --- packages/ckeditor5-core/src/editor/editor.ts | 31 +++++++------------ .../tests/editor/licensecheck.js | 6 ++-- packages/ckeditor5-utils/src/crc32.ts | 6 ++-- .../tests/parsebase64encodedobject.js | 6 ++++ 4 files changed, 23 insertions(+), 26 deletions(-) diff --git a/packages/ckeditor5-core/src/editor/editor.ts b/packages/ckeditor5-core/src/editor/editor.ts index 664a5952974..80cf406c42f 100644 --- a/packages/ckeditor5-core/src/editor/editor.ts +++ b/packages/ckeditor5-core/src/editor/editor.ts @@ -44,7 +44,7 @@ import type { EditorConfig } from './editorconfig.js'; declare global { // eslint-disable-next-line no-var - var CKEDITOR_GLOBAL_LICENSE_KEY: string; + var CKEDITOR_GLOBAL_LICENSE_KEY: string | undefined; } /** @@ -356,12 +356,13 @@ export default abstract class Editor extends /* #__PURE__ */ ObservableMixin() { let licenseKey = config.get( 'licenseKey' ); if ( !licenseKey && window.CKEDITOR_GLOBAL_LICENSE_KEY ) { - config.set( 'licenseKey', licenseKey = window.CKEDITOR_GLOBAL_LICENSE_KEY ); + licenseKey = window.CKEDITOR_GLOBAL_LICENSE_KEY; + config.set( 'licenseKey', licenseKey ); } if ( !licenseKey ) { /** - * The license key is missing. + * The licenseKey is missing. Add your license or 'GPL' string to the editor config. * * @error editor-license-key-missing */ @@ -371,7 +372,7 @@ export default abstract class Editor extends /* #__PURE__ */ ObservableMixin() { function verifyLicenseKey( editor: Editor ) { const licenseKey = editor.config.get( 'licenseKey' )!; - const distributionChannel = ( window as any )[ ' CKE_DISTRIBUTION' ] || 'sh'; + const distributionChannel = ( window as any )[ 'CKE_DISTRIBUTION ' ] || 'sh'; if ( licenseKey == 'GPL' ) { if ( distributionChannel == 'cloud' ) { @@ -428,11 +429,11 @@ export default abstract class Editor extends /* #__PURE__ */ ObservableMixin() { if ( licensedHosts ) { const hostname = window.location.hostname; const willcards = licensedHosts - .filter( val => val.slice( 0, 1 ) === '*' ) - .map( val => val.slice( 1 ) ); + .filter( val => val.startsWith( '*' ) ) + .map( val => val.substring( 1 ) ); const isHostnameMatched = licensedHosts.some( licensedHost => licensedHost === hostname ); - const isWillcardMatched = willcards.some( willcard => willcard === hostname.slice( -willcard.length ) ); + const isWillcardMatched = willcards.some( willcard => hostname.endsWith( willcard ) ); if ( !isWillcardMatched && !isHostnameMatched ) { blockEditor( 'domainLimit' ); @@ -442,7 +443,7 @@ export default abstract class Editor extends /* #__PURE__ */ ObservableMixin() { } if ( licensePayload.licenseType === 'trial' && licensePayload.exp * 1000 < Date.now() ) { - blockEditor( 'trialLimit' ); + blockEditor( 'expired' ); return; } @@ -457,7 +458,7 @@ export default abstract class Editor extends /* #__PURE__ */ ObservableMixin() { const timerId = setTimeout( () => { blockEditor( `${ licenseType }Limit` ); - }, 600000 /* 10 minutes */ ); + }, 600000 ); editor.on( 'destroy', () => { clearTimeout( timerId ); @@ -519,9 +520,6 @@ export default abstract class Editor extends /* #__PURE__ */ ObservableMixin() { return requiredFields.every( field => field in licensePayload ); } - /** - * Returns an array of values that are used to calculate the CRC32 checksum. - */ function getCrcInputData( licensePayload: Record ): CRCData { const keysToCheck = Object.getOwnPropertyNames( licensePayload ).sort(); @@ -529,7 +527,7 @@ export default abstract class Editor extends /* #__PURE__ */ ObservableMixin() { .filter( key => key != 'vc' && licensePayload[ key ] != null ) .map( key => licensePayload[ key ] ); - return [ ...filteredValues ] as CRCData; + return filteredValues as CRCData; } } } @@ -847,11 +845,7 @@ export default abstract class Editor extends /* #__PURE__ */ ObservableMixin() { }; } - /** - * @internal - */ private _showLicenseError( reason: LicenseErrorReason, pluginName?: string ) { - // Make sure the error thrown is unhandled. setTimeout( () => { if ( reason == 'invalid' ) { /** @@ -944,9 +938,6 @@ export default abstract class Editor extends /* #__PURE__ */ ObservableMixin() { this._showLicenseError = () => {}; } - /** - * @internal - */ private async _sendUsageRequest( endpoint: string, request: unknown ) { const response = await fetch( new URL( endpoint ), { method: 'POST', diff --git a/packages/ckeditor5-core/tests/editor/licensecheck.js b/packages/ckeditor5-core/tests/editor/licensecheck.js index 4e4d4c86ea7..c0394ccb016 100644 --- a/packages/ckeditor5-core/tests/editor/licensecheck.js +++ b/packages/ckeditor5-core/tests/editor/licensecheck.js @@ -111,7 +111,7 @@ describe( 'Editor - license check', () => { describe( 'distribution channel check', () => { afterEach( () => { - delete window[ ' CKE_DISTRIBUTION' ]; + delete window[ 'CKE_DISTRIBUTION ' ]; } ); it( 'should not block if distribution channel match', () => { @@ -210,7 +210,7 @@ describe( 'Editor - license check', () => { } ); function setChannel( channel ) { - window[ ' CKE_DISTRIBUTION' ] = channel; + window[ 'CKE_DISTRIBUTION ' ] = channel; } } ); @@ -283,7 +283,7 @@ describe( 'Editor - license check', () => { const editor = new TestEditor( { licenseKey } ); - sinon.assert.calledWithMatch( showErrorStub, 'trialLimit' ); + sinon.assert.calledWithMatch( showErrorStub, 'expired' ); expect( editor.isReadOnly ).to.be.true; dateNow.restore(); diff --git a/packages/ckeditor5-utils/src/crc32.ts b/packages/ckeditor5-utils/src/crc32.ts index e5d7242e0dc..4734b638a5d 100644 --- a/packages/ckeditor5-utils/src/crc32.ts +++ b/packages/ckeditor5-utils/src/crc32.ts @@ -51,7 +51,7 @@ export default function crc32( inputData: CRCData ): string { const crcTable: Array = makeCrcTable(); let crc: number = 0 ^ ( -1 ); - // Convert data to a single string + // Convert data to a single string. const dataString: string = dataArray.map( item => { if ( Array.isArray( item ) ) { return item.join( '' ); @@ -60,13 +60,13 @@ export default function crc32( inputData: CRCData ): string { return String( item ); } ).join( '' ); - // Calculate the CRC for the resulting string + // Calculate the CRC for the resulting string. for ( let i = 0; i < dataString.length; i++ ) { const byte: number = dataString.charCodeAt( i ); crc = ( crc >>> 8 ) ^ crcTable[ ( crc ^ byte ) & 0xFF ]; } - crc = ( crc ^ ( -1 ) ) >>> 0; // Force unsigned integer + crc = ( crc ^ ( -1 ) ) >>> 0; // Force unsigned integer. return crc.toString( 16 ).padStart( 8, '0' ); } diff --git a/packages/ckeditor5-utils/tests/parsebase64encodedobject.js b/packages/ckeditor5-utils/tests/parsebase64encodedobject.js index e3865412d54..e673b4742b6 100644 --- a/packages/ckeditor5-utils/tests/parsebase64encodedobject.js +++ b/packages/ckeditor5-utils/tests/parsebase64encodedobject.js @@ -27,4 +27,10 @@ describe( 'parseBase64EncodedObject', () => { expect( parseBase64EncodedObject( encoded ) ).to.be.null; } ); + + it( 'should use base64Safe variant of encoding', () => { + const encoded = 'eyJmb28iOiJhYmNkZW/n+Glqa2xtbm8ifQ=='; + + expect( parseBase64EncodedObject( encoded ) ).to.deep.equal( { foo: 'abcdeoçøijklmno' } ); + } ); } ); From 0f1335317523e5916888c7d3e51e5288942a79eb Mon Sep 17 00:00:00 2001 From: Kuba Niegowski Date: Tue, 25 Jun 2024 20:28:02 +0200 Subject: [PATCH 056/256] Extracted abstract Badge UI helper. --- .../ckeditor5-core/src/editor/editorconfig.ts | 10 +- packages/ckeditor5-ui/src/badge/badge.ts | 347 ++++++++++++++++++ .../ckeditor5-ui/src/editorui/poweredby.ts | 288 ++------------- 3 files changed, 388 insertions(+), 257 deletions(-) create mode 100644 packages/ckeditor5-ui/src/badge/badge.ts diff --git a/packages/ckeditor5-core/src/editor/editorconfig.ts b/packages/ckeditor5-core/src/editor/editorconfig.ts index bc78bf29b23..ffc95c1ce90 100644 --- a/packages/ckeditor5-core/src/editor/editorconfig.ts +++ b/packages/ckeditor5-core/src/editor/editorconfig.ts @@ -894,7 +894,7 @@ export interface PoweredByConfig { * * @default 'border' */ - position: 'inside' | 'border'; + position?: 'inside' | 'border'; /** * Allows choosing the side of the editing area where the logo will be displayed. @@ -904,7 +904,7 @@ export interface PoweredByConfig { * * @default 'right' */ - side: 'left' | 'right'; + side?: 'left' | 'right'; /** * Allows changing the label displayed next to the CKEditor logo. @@ -913,7 +913,7 @@ export interface PoweredByConfig { * * @default 'Powered by' */ - label: string | null; + label?: string | null; /** * The vertical distance the logo can be moved away from its default position. @@ -922,14 +922,14 @@ export interface PoweredByConfig { * * @default 5 */ - verticalOffset: number; + verticalOffset?: number; /** * The horizontal distance between the side of the editing root and the nearest side of the logo. * * @default 5 */ - horizontalOffset: number; + horizontalOffset?: number; /** * Allows to show the logo even if the valid commercial license is configured using diff --git a/packages/ckeditor5-ui/src/badge/badge.ts b/packages/ckeditor5-ui/src/badge/badge.ts new file mode 100644 index 00000000000..3b63f07439b --- /dev/null +++ b/packages/ckeditor5-ui/src/badge/badge.ts @@ -0,0 +1,347 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/** + * @module ui/badge/badge + */ + +import type { Editor } from '@ckeditor/ckeditor5-core'; + +import { + Rect, + DomEmitterMixin, + type PositionOptions +} from '@ckeditor/ckeditor5-utils'; + +import type View from '../view.js'; +import BalloonPanelView from '../panel/balloon/balloonpanelview.js'; + +import { throttle } from 'lodash-es'; + +// ⚠ Note, whenever changing the threshold, make sure to update the docs/support/managing-ckeditor-logo.md docs +// as this information is also mentioned there ⚠. +const NARROW_ROOT_HEIGHT_THRESHOLD = 50; +const NARROW_ROOT_WIDTH_THRESHOLD = 350; + +/** + * A helper that enables the badge feature in the editor and renders a custom view next to the bottom of the editable element + * (editor root, source editing area, etc.) when the editor is focused. + * + * @private + */ +export default abstract class Badge extends /* #__PURE__ */ DomEmitterMixin() { + /** + * Editor instance the helper was created for. + */ + protected readonly editor: Editor; + + /** + * A reference to the balloon panel hosting and positioning the "powered by" link and logo. + */ + private _balloonView: BalloonPanelView | null = null; + + /** + * A throttled version of the {@link #_showBalloon} method meant for frequent use to avoid performance loss. + */ + private _showBalloonThrottled = throttle( () => this._showBalloon(), 50, { leading: true } ); + + /** + * A reference to the last editable element (root, source editing area, etc.) focused by the user. + * Since the focus can move to other focusable elements in the UI, this reference allows positioning the balloon over the + * right element whether the user is typing or using the UI. + */ + private _lastFocusedEditableElement: HTMLElement | null = null; + + /** + * An additional CSS class added to the `BalloonView`. + */ + private readonly _balloonClass: string | undefined; + + /** + * Creates a badge for a given editor. The feature is initialized on Editor#ready + * event. + */ + protected constructor( editor: Editor, options: { balloonClass?: string } = {} ) { + super(); + + this.editor = editor; + this._balloonClass = options.balloonClass; + + editor.on( 'ready', () => this._handleEditorReady() ); + } + + /** + * Destroys the badge along with its view. + */ + public destroy(): void { + const balloon = this._balloonView; + + if ( balloon ) { + // Balloon gets destroyed by the body collection. + // The powered by view gets destroyed by the balloon. + balloon.unpin(); + this._balloonView = null; + } + + this._showBalloonThrottled.cancel(); + this.stopListening(); + } + + /** + * Enables "powered by" label once the editor (ui) is ready. + */ + protected _handleEditorReady(): void { + const editor = this.editor; + + if ( !this._isEnabled() ) { + return; + } + + // No view means no body collection to append the powered by balloon to. + if ( !editor.ui.view ) { + return; + } + + editor.ui.focusTracker.on( 'change:isFocused', ( evt, data, isFocused ) => { + this._updateLastFocusedEditableElement(); + + if ( isFocused ) { + this._showBalloon(); + } else { + this._hideBalloon(); + } + } ); + + editor.ui.focusTracker.on( 'change:focusedElement', ( evt, data, focusedElement ) => { + this._updateLastFocusedEditableElement(); + + if ( focusedElement ) { + this._showBalloon(); + } + } ); + + editor.ui.on( 'update', () => { + this._showBalloonThrottled(); + } ); + } + + /** + * TODO + */ + protected _getNormalizedConfig(): BadgeConfig { + return { + side: this.editor.locale.contentLanguageDirection === 'ltr' ? 'right' : 'left', + position: 'border', + verticalOffset: 0, + horizontalOffset: 5 + }; + } + + /** + * TODO + */ + protected abstract _createBadgeContent(): View; + + /** + * TODO + */ + protected abstract _isEnabled(): boolean; + + /** + * Attempts to display the balloon with the "powered by" view. + */ + private _showBalloon(): void { + const attachOptions = this._getBalloonAttachOptions(); + + if ( !attachOptions ) { + return; + } + + if ( !this._balloonView ) { + this._balloonView = this._createBalloonView(); + } + + this._balloonView.pin( attachOptions ); + } + + /** + * Hides the "powered by" balloon if already visible. + */ + private _hideBalloon(): void { + if ( this._balloonView ) { + this._balloonView.unpin(); + } + } + + /** + * Creates an instance of the {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView balloon panel} + * with the "powered by" view inside ready for positioning. + */ + private _createBalloonView(): BalloonPanelView { + const editor = this.editor; + const balloon = new BalloonPanelView(); + const view = this._createBadgeContent(); + + balloon.content.add( view ); + + if ( this._balloonClass ) { + balloon.class = this._balloonClass; + } + + editor.ui.view.body.add( balloon ); + editor.ui.focusTracker.add( balloon.element! ); + + return balloon; + } + + /** + * TODO + */ + private _getBalloonAttachOptions(): Partial | null { + if ( !this._lastFocusedEditableElement ) { + return null; + } + + const badgeConfig = this._getNormalizedConfig(); + + const positioningFunction = badgeConfig.side === 'right' ? + getLowerRightCornerPosition( this._lastFocusedEditableElement, badgeConfig ) : + getLowerLeftCornerPosition( this._lastFocusedEditableElement, badgeConfig ); + + return { + target: this._lastFocusedEditableElement, + positions: [ positioningFunction ] + }; + } + + /** + * Updates the {@link #_lastFocusedEditableElement} based on the state of the global focus tracker. + */ + private _updateLastFocusedEditableElement(): void { + const editor = this.editor; + const isFocused = editor.ui.focusTracker.isFocused; + const focusedElement = editor.ui.focusTracker.focusedElement! as HTMLElement; + + if ( !isFocused || !focusedElement ) { + this._lastFocusedEditableElement = null; + + return; + } + + const editableEditorElements = Array.from( editor.ui.getEditableElementsNames() ).map( name => { + return editor.ui.getEditableElement( name ); + } ); + + if ( editableEditorElements.includes( focusedElement ) ) { + this._lastFocusedEditableElement = focusedElement; + } else { + // If it's none of the editable element, then the focus is somewhere in the UI. Let's display powered by + // over the first element then. + this._lastFocusedEditableElement = editableEditorElements[ 0 ]!; + } + } +} + +function getLowerRightCornerPosition( focusedEditableElement: HTMLElement, config: BadgeConfig ) { + return getLowerCornerPosition( focusedEditableElement, config, ( rootRect, balloonRect ) => { + return rootRect.left + rootRect.width - balloonRect.width - config.horizontalOffset; + } ); +} + +function getLowerLeftCornerPosition( focusedEditableElement: HTMLElement, config: BadgeConfig ) { + return getLowerCornerPosition( focusedEditableElement, config, rootRect => rootRect.left + config.horizontalOffset ); +} + +function getLowerCornerPosition( + focusedEditableElement: HTMLElement, + config: BadgeConfig, + getBalloonLeft: ( visibleEditableElementRect: Rect, balloonRect: Rect ) => number +) { + return ( visibleEditableElementRect: Rect, balloonRect: Rect ) => { + const editableElementRect = new Rect( focusedEditableElement ); + + if ( editableElementRect.width < NARROW_ROOT_WIDTH_THRESHOLD || editableElementRect.height < NARROW_ROOT_HEIGHT_THRESHOLD ) { + return null; + } + + let balloonTop; + + if ( config.position === 'inside' ) { + balloonTop = editableElementRect.bottom - balloonRect.height; + } + else { + balloonTop = editableElementRect.bottom - balloonRect.height / 2; + } + + balloonTop -= config.verticalOffset; + + const balloonLeft = getBalloonLeft( editableElementRect, balloonRect ); + + // Clone the editable element rect and place it where the balloon would be placed. + // This will allow getVisible() to work from editable element's perspective (rect source). + // and yield a result as if the balloon was on the same (scrollable) layer as the editable element. + const newBalloonPositionRect = visibleEditableElementRect + .clone() + .moveTo( balloonLeft, balloonTop ) + .getIntersection( balloonRect.clone().moveTo( balloonLeft, balloonTop ) )!; + + const newBalloonPositionVisibleRect = newBalloonPositionRect.getVisible(); + + if ( !newBalloonPositionVisibleRect || newBalloonPositionVisibleRect.getArea() < balloonRect.getArea() ) { + return null; + } + + return { + top: balloonTop, + left: balloonLeft, + name: `position_${ config.position }-side_${ config.side }`, + config: { + withArrow: false + } + }; + }; +} + +/** + * The badge configuration options. + **/ +export interface BadgeConfig { + + /** + * The position of the badge. + * + * * When `'inside'`, the badge will be displayed within the boundaries of the editing area. + * * When `'border'`, the basge will be displayed over the bottom border of the editing area. + * + * @default 'border' + */ + position: 'inside' | 'border'; + + /** + * Allows choosing the side of the editing area where the badge will be displayed. + * + * **Note:** If {@link module:core/editor/editorconfig~EditorConfig#language `config.language`} is set to an RTL (right-to-left) + * language, the side switches to `'left'` by default. + * + * @default 'right' + */ + side: 'left' | 'right'; + + /** + * The vertical distance the badge can be moved away from its default position. + * + * **Note:** If `position` is `'border'`, the offset is measured from the (vertical) center of the badge. + * + * @default 5 + */ + verticalOffset: number; + + /** + * The horizontal distance between the side of the editing root and the nearest side of the badge. + * + * @default 5 + */ + horizontalOffset: number; +} diff --git a/packages/ckeditor5-ui/src/editorui/poweredby.ts b/packages/ckeditor5-ui/src/editorui/poweredby.ts index fcb41f41162..ef8b8d61bbc 100644 --- a/packages/ckeditor5-ui/src/editorui/poweredby.ts +++ b/packages/ckeditor5-ui/src/editorui/poweredby.ts @@ -8,26 +8,17 @@ */ import type { Editor, UiConfig } from '@ckeditor/ckeditor5-core'; -import { - DomEmitterMixin, - Rect, - parseBase64EncodedObject, - type PositionOptions, - type Locale -} from '@ckeditor/ckeditor5-utils'; -import BalloonPanelView from '../panel/balloon/balloonpanelview.js'; -import IconView from '../icon/iconview.js'; +import { parseBase64EncodedObject, type Locale } from '@ckeditor/ckeditor5-utils'; + import View from '../view.js'; -import { throttle, type DebouncedFunc } from 'lodash-es'; +import Badge from '../badge/badge.js'; +import IconView from '../icon/iconview.js'; import poweredByIcon from '../../theme/icons/project-logo.svg'; const ICON_WIDTH = 53; const ICON_HEIGHT = 10; -// ⚠ Note, whenever changing the threshold, make sure to update the docs/support/managing-ckeditor-logo.md docs -// as this information is also mentioned there ⚠. -const NARROW_ROOT_HEIGHT_THRESHOLD = 50; -const NARROW_ROOT_WIDTH_THRESHOLD = 350; + const DEFAULT_LABEL = 'Powered by'; type PoweredByConfig = Required[ 'poweredBy' ]; @@ -38,181 +29,61 @@ type PoweredByConfig = Required[ 'poweredBy' ]; * * @private */ -export default class PoweredBy extends /* #__PURE__ */ DomEmitterMixin() { - /** - * Editor instance the helper was created for. - */ - private readonly editor: Editor; - - /** - * A reference to the balloon panel hosting and positioning the "powered by" link and logo. - */ - private _balloonView: BalloonPanelView | null; - - /** - * A throttled version of the {@link #_showBalloon} method meant for frequent use to avoid performance loss. - */ - private _showBalloonThrottled: DebouncedFunc<() => void>; - - /** - * A reference to the last editable element (root, source editing area, etc.) focused by the user. - * Since the focus can move to other focusable elements in the UI, this reference allows positioning the balloon over the - * right element whether the user is typing or using the UI. - */ - private _lastFocusedEditableElement: HTMLElement | null; - - /** - * Creates a "powered by" helper for a given editor. The feature is initialized on Editor#ready - * event. - * - * @param editor - */ +export default class PoweredBy extends Badge { constructor( editor: Editor ) { - super(); - - this.editor = editor; - this._balloonView = null; - this._lastFocusedEditableElement = null; - this._showBalloonThrottled = throttle( this._showBalloon.bind( this ), 50, { leading: true } ); - - editor.on( 'ready', this._handleEditorReady.bind( this ) ); - } - - /** - * Destroys the "powered by" helper along with its view. - */ - public destroy(): void { - const balloon = this._balloonView; - - if ( balloon ) { - // Balloon gets destroyed by the body collection. - // The powered by view gets destroyed by the balloon. - balloon.unpin(); - this._balloonView = null; - } - - this._showBalloonThrottled.cancel(); - this.stopListening(); + super( editor, { balloonClass: 'ck-powered-by-balloon' } ); } /** - * Enables "powered by" label once the editor (ui) is ready. + * Enables "powered by" label. */ - private _handleEditorReady(): void { + protected override _isEnabled(): boolean { const editor = this.editor; const forceVisible = editor.config.get( 'ui.poweredBy.forceVisible' ); - if ( !forceVisible ) { - const licenseKey = editor.config.get( 'licenseKey' )!; - const licenseContent = licenseKey != 'GPL' && parseBase64EncodedObject( licenseKey.split( '.' )[ 1 ] ); - - if ( licenseContent && licenseContent.whiteLabel ) { - return; - } - } - - // No view means no body collection to append the powered by balloon to. - if ( !editor.ui.view ) { - return; + if ( forceVisible ) { + return true; } - editor.ui.focusTracker.on( 'change:isFocused', ( evt, data, isFocused ) => { - this._updateLastFocusedEditableElement(); - - if ( isFocused ) { - this._showBalloon(); - } else { - this._hideBalloon(); - } - } ); - - editor.ui.focusTracker.on( 'change:focusedElement', ( evt, data, focusedElement ) => { - this._updateLastFocusedEditableElement(); - - if ( focusedElement ) { - this._showBalloon(); - } - } ); - - editor.ui.on( 'update', () => { - this._showBalloonThrottled(); - } ); - } - - /** - * Creates an instance of the {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView balloon panel} - * with the "powered by" view inside ready for positioning. - */ - private _createBalloonView(): void { - const editor = this.editor; - const balloon = this._balloonView = new BalloonPanelView(); - const poweredByConfig = getNormalizedConfig( editor ); - const view = new PoweredByView( editor.locale, poweredByConfig.label ); - - balloon.content.add( view ); - balloon.set( { - class: 'ck-powered-by-balloon' - } ); + const licenseKey = editor.config.get( 'licenseKey' )!; - editor.ui.view.body.add( balloon ); - editor.ui.focusTracker.add( balloon.element! ); - - this._balloonView = balloon; - } - - /** - * Attempts to display the balloon with the "powered by" view. - */ - private _showBalloon(): void { - if ( !this._lastFocusedEditableElement ) { - return; + if ( licenseKey == 'GPL' ) { + return true; } - const attachOptions = getBalloonAttachOptions( this.editor, this._lastFocusedEditableElement ); - - if ( attachOptions ) { - if ( !this._balloonView ) { - this._createBalloonView(); - } + const licenseContent = parseBase64EncodedObject( licenseKey.split( '.' )[ 1 ] ); - this._balloonView!.pin( attachOptions ); + if ( !licenseContent ) { + return true; } + + return !licenseContent.whiteLabel; } /** - * Hides the "powered by" balloon if already visible. + * TODO */ - private _hideBalloon(): void { - if ( this._balloonView ) { - this._balloonView!.unpin(); - } + protected override _createBadgeContent(): View { + return new PoweredByView( this.editor.locale, this._getNormalizedConfig().label ); } /** - * Updates the {@link #_lastFocusedEditableElement} based on the state of the global focus tracker. + * TODO */ - private _updateLastFocusedEditableElement(): void { - const editor = this.editor; - const isFocused = editor.ui.focusTracker.isFocused; - const focusedElement = editor.ui.focusTracker.focusedElement! as HTMLElement; - - if ( !isFocused || !focusedElement ) { - this._lastFocusedEditableElement = null; - - return; - } - - const editableEditorElements = Array.from( editor.ui.getEditableElementsNames() ).map( name => { - return editor.ui.getEditableElement( name ); - } ); + protected override _getNormalizedConfig(): Required { + const badgeConfig = super._getNormalizedConfig(); + const userConfig = this.editor.config.get( 'ui.poweredBy' ) || {}; + const position = userConfig.position || badgeConfig.position; + const verticalOffset = position === 'inside' ? 5 : badgeConfig.verticalOffset; - if ( editableEditorElements.includes( focusedElement ) ) { - this._lastFocusedEditableElement = focusedElement; - } else { - // If it's none of the editable element, then the focus is somewhere in the UI. Let's display powered by - // over the first element then. - this._lastFocusedEditableElement = editableEditorElements[ 0 ]!; - } + return { + position, + side: userConfig.side || badgeConfig.side, + label: userConfig.label === undefined ? DEFAULT_LABEL : userConfig.label, + verticalOffset: userConfig.verticalOffset !== undefined ? userConfig.verticalOffset : verticalOffset, + horizontalOffset: userConfig.horizontalOffset !== undefined ? userConfig.horizontalOffset : badgeConfig.horizontalOffset, + forceVisible: !!userConfig.forceVisible + }; } } @@ -281,90 +152,3 @@ class PoweredByView extends View { } ); } } - -function getBalloonAttachOptions( editor: Editor, focusedEditableElement: HTMLElement ): Partial | null { - const poweredByConfig = getNormalizedConfig( editor )!; - const positioningFunction = poweredByConfig.side === 'right' ? - getLowerRightCornerPosition( focusedEditableElement, poweredByConfig ) : - getLowerLeftCornerPosition( focusedEditableElement, poweredByConfig ); - - return { - target: focusedEditableElement, - positions: [ positioningFunction ] - }; -} - -function getLowerRightCornerPosition( focusedEditableElement: HTMLElement, config: PoweredByConfig ) { - return getLowerCornerPosition( focusedEditableElement, config, ( rootRect, balloonRect ) => { - return rootRect.left + rootRect.width - balloonRect.width - config.horizontalOffset; - } ); -} - -function getLowerLeftCornerPosition( focusedEditableElement: HTMLElement, config: PoweredByConfig ) { - return getLowerCornerPosition( focusedEditableElement, config, rootRect => rootRect.left + config.horizontalOffset ); -} - -function getLowerCornerPosition( - focusedEditableElement: HTMLElement, - config: PoweredByConfig, - getBalloonLeft: ( visibleEditableElementRect: Rect, balloonRect: Rect ) => number -) { - return ( visibleEditableElementRect: Rect, balloonRect: Rect ) => { - const editableElementRect = new Rect( focusedEditableElement ); - - if ( editableElementRect.width < NARROW_ROOT_WIDTH_THRESHOLD || editableElementRect.height < NARROW_ROOT_HEIGHT_THRESHOLD ) { - return null; - } - - let balloonTop; - - if ( config.position === 'inside' ) { - balloonTop = editableElementRect.bottom - balloonRect.height; - } - else { - balloonTop = editableElementRect.bottom - balloonRect.height / 2; - } - - balloonTop -= config.verticalOffset; - - const balloonLeft = getBalloonLeft( editableElementRect, balloonRect ); - - // Clone the editable element rect and place it where the balloon would be placed. - // This will allow getVisible() to work from editable element's perspective (rect source). - // and yield a result as if the balloon was on the same (scrollable) layer as the editable element. - const newBalloonPositionRect = visibleEditableElementRect - .clone() - .moveTo( balloonLeft, balloonTop ) - .getIntersection( balloonRect.clone().moveTo( balloonLeft, balloonTop ) )!; - - const newBalloonPositionVisibleRect = newBalloonPositionRect.getVisible(); - - if ( !newBalloonPositionVisibleRect || newBalloonPositionVisibleRect.getArea() < balloonRect.getArea() ) { - return null; - } - - return { - top: balloonTop, - left: balloonLeft, - name: `position_${ config.position }-side_${ config.side }`, - config: { - withArrow: false - } - }; - }; -} - -function getNormalizedConfig( editor: Editor ): PoweredByConfig { - const userConfig = editor.config.get( 'ui.poweredBy' ); - const position = userConfig && userConfig.position || 'border'; - - return { - position, - label: DEFAULT_LABEL, - verticalOffset: position === 'inside' ? 5 : 0, - horizontalOffset: 5, - - side: editor.locale.contentLanguageDirection === 'ltr' ? 'right' : 'left', - ...userConfig - }; -} From c381dd5d3537fb96bc34de30dd0ff29aaf07f7c0 Mon Sep 17 00:00:00 2001 From: Marta Motyczynska Date: Wed, 26 Jun 2024 12:19:22 +0200 Subject: [PATCH 057/256] Fix tests for poweredby with whiteLabel in license key. --- packages/ckeditor5-core/tests/editor/licensecheck.js | 4 ++-- packages/ckeditor5-ui/tests/editorui/poweredby.js | 10 +++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/ckeditor5-core/tests/editor/licensecheck.js b/packages/ckeditor5-core/tests/editor/licensecheck.js index c0394ccb016..f6e3a4b8716 100644 --- a/packages/ckeditor5-core/tests/editor/licensecheck.js +++ b/packages/ckeditor5-core/tests/editor/licensecheck.js @@ -641,7 +641,7 @@ function wait( time ) { } ); } -function generateKey( options = {} ) { +export function generateKey( options = {} ) { const { isExpired = false, jtiExist = true, @@ -664,7 +664,7 @@ function generateKey( options = {} ) { const payload = {}; - [ 'licensedHosts', 'licenseType', 'usageEndpoint', 'distributionChannel' ].forEach( prop => { + [ 'licensedHosts', 'licenseType', 'usageEndpoint', 'distributionChannel', 'whiteLabel' ].forEach( prop => { if ( prop in options ) { payload[ prop ] = options[ prop ]; } diff --git a/packages/ckeditor5-ui/tests/editorui/poweredby.js b/packages/ckeditor5-ui/tests/editorui/poweredby.js index 513b84e9136..563cb400eae 100644 --- a/packages/ckeditor5-ui/tests/editorui/poweredby.js +++ b/packages/ckeditor5-ui/tests/editorui/poweredby.js @@ -17,6 +17,7 @@ import { Rect, global } from '@ckeditor/ckeditor5-utils'; import SourceEditing from '@ckeditor/ckeditor5-source-editing/src/sourceediting.js'; import Heading from '@ckeditor/ckeditor5-heading/src/heading.js'; import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model.js'; +import { generateKey } from '@ckeditor/ckeditor5-core/tests/editor/licensecheck.js'; describe( 'PoweredBy', () => { let editor, element; @@ -98,8 +99,9 @@ describe( 'PoweredBy', () => { } ); it( 'should not create the balloon when a white-label license key is configured', async () => { + const { licenseKey } = generateKey( { whiteLabel: true } ); const editor = await createEditor( element, { - licenseKey: 'foo.eyJ3aGl0ZUxhYmVsIjp0cnVlLCJleHAiOjIyMDg5ODg4MDB9.bar' + licenseKey } ); expect( editor.ui.poweredBy._balloonView ).to.be.null; @@ -112,8 +114,9 @@ describe( 'PoweredBy', () => { } ); it( 'should create the balloon when a white-label license key is configured and `forceVisible` is set to true', async () => { + const { licenseKey } = generateKey( { whiteLabel: true } ); const editor = await createEditor( element, { - licenseKey: 'foo.eyJ3aGl0ZUxhYmVsIjp0cnVlLCJleHAiOjIyMDg5ODg4MDB9.bar', + licenseKey, ui: { poweredBy: { forceVisible: true @@ -131,8 +134,9 @@ describe( 'PoweredBy', () => { } ); it( 'should create the balloon when a non-white-label license key is configured', async () => { + const { licenseKey } = generateKey(); const editor = await createEditor( element, { - licenseKey: 'foo.eyJhYmMiOjEsImV4cCI6MjIwODk4ODgwMH0.bar' + licenseKey } ); expect( editor.ui.poweredBy._balloonView ).to.be.null; From 1afc755053732a888a107e37fcf06caf8d8c2752 Mon Sep 17 00:00:00 2001 From: Marta Motyczynska Date: Wed, 26 Jun 2024 21:13:41 +0200 Subject: [PATCH 058/256] Add evaluation badge. Add missing api docs. --- packages/ckeditor5-ui/src/badge/badge.ts | 24 ++-- .../ckeditor5-ui/src/editorui/editorui.ts | 8 ++ .../src/editorui/evaluationbadge.ts | 115 ++++++++++++++++++ .../ckeditor5-ui/src/editorui/poweredby.ts | 7 +- .../theme/globals/_evaluationbadge.css | 48 ++++++++ .../ckeditor5-ui/theme/globals/globals.css | 1 + 6 files changed, 188 insertions(+), 15 deletions(-) create mode 100644 packages/ckeditor5-ui/src/editorui/evaluationbadge.ts create mode 100644 packages/ckeditor5-ui/theme/globals/_evaluationbadge.css diff --git a/packages/ckeditor5-ui/src/badge/badge.ts b/packages/ckeditor5-ui/src/badge/badge.ts index 3b63f07439b..cc522278baa 100644 --- a/packages/ckeditor5-ui/src/badge/badge.ts +++ b/packages/ckeditor5-ui/src/badge/badge.ts @@ -38,7 +38,7 @@ export default abstract class Badge extends /* #__PURE__ */ DomEmitterMixin() { protected readonly editor: Editor; /** - * A reference to the balloon panel hosting and positioning the "powered by" link and logo. + * A reference to the balloon panel hosting and positioning the badge content. */ private _balloonView: BalloonPanelView | null = null; @@ -80,7 +80,7 @@ export default abstract class Badge extends /* #__PURE__ */ DomEmitterMixin() { if ( balloon ) { // Balloon gets destroyed by the body collection. - // The powered by view gets destroyed by the balloon. + // The badge view gets destroyed by the balloon. balloon.unpin(); this._balloonView = null; } @@ -90,7 +90,7 @@ export default abstract class Badge extends /* #__PURE__ */ DomEmitterMixin() { } /** - * Enables "powered by" label once the editor (ui) is ready. + * Enables badge label once the editor (ui) is ready. */ protected _handleEditorReady(): void { const editor = this.editor; @@ -99,7 +99,7 @@ export default abstract class Badge extends /* #__PURE__ */ DomEmitterMixin() { return; } - // No view means no body collection to append the powered by balloon to. + // No view means no body collection to append the badge balloon to. if ( !editor.ui.view ) { return; } @@ -128,7 +128,7 @@ export default abstract class Badge extends /* #__PURE__ */ DomEmitterMixin() { } /** - * TODO + * Returns normalized configuration for the badge. */ protected _getNormalizedConfig(): BadgeConfig { return { @@ -140,17 +140,17 @@ export default abstract class Badge extends /* #__PURE__ */ DomEmitterMixin() { } /** - * TODO + * Creates the badge content. */ protected abstract _createBadgeContent(): View; /** - * TODO + * Enables the badge feature. */ protected abstract _isEnabled(): boolean; /** - * Attempts to display the balloon with the "powered by" view. + * Attempts to display the balloon with the badge view. */ private _showBalloon(): void { const attachOptions = this._getBalloonAttachOptions(); @@ -167,7 +167,7 @@ export default abstract class Badge extends /* #__PURE__ */ DomEmitterMixin() { } /** - * Hides the "powered by" balloon if already visible. + * Hides the badge balloon if already visible. */ private _hideBalloon(): void { if ( this._balloonView ) { @@ -177,7 +177,7 @@ export default abstract class Badge extends /* #__PURE__ */ DomEmitterMixin() { /** * Creates an instance of the {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView balloon panel} - * with the "powered by" view inside ready for positioning. + * with the badge view inside ready for positioning. */ private _createBalloonView(): BalloonPanelView { const editor = this.editor; @@ -197,7 +197,7 @@ export default abstract class Badge extends /* #__PURE__ */ DomEmitterMixin() { } /** - * TODO + * Returns the options for attaching the balloon to the focused editable element. */ private _getBalloonAttachOptions(): Partial | null { if ( !this._lastFocusedEditableElement ) { @@ -237,7 +237,7 @@ export default abstract class Badge extends /* #__PURE__ */ DomEmitterMixin() { if ( editableEditorElements.includes( focusedElement ) ) { this._lastFocusedEditableElement = focusedElement; } else { - // If it's none of the editable element, then the focus is somewhere in the UI. Let's display powered by + // If it's none of the editable element, then the focus is somewhere in the UI. Let's display the badge // over the first element then. this._lastFocusedEditableElement = editableEditorElements[ 0 ]!; } diff --git a/packages/ckeditor5-ui/src/editorui/editorui.ts b/packages/ckeditor5-ui/src/editorui/editorui.ts index 1b73e8b376e..7d969d273c6 100644 --- a/packages/ckeditor5-ui/src/editorui/editorui.ts +++ b/packages/ckeditor5-ui/src/editorui/editorui.ts @@ -12,6 +12,7 @@ import ComponentFactory from '../componentfactory.js'; import TooltipManager from '../tooltipmanager.js'; import PoweredBy from './poweredby.js'; +import EvaluationBadge from './evaluationbadge.js'; import AriaLiveAnnouncer from '../arialiveannouncer.js'; import type EditorUIView from './editoruiview.js'; @@ -59,6 +60,11 @@ export default abstract class EditorUI extends /* #__PURE__ */ ObservableMixin() */ public readonly poweredBy: PoweredBy; + /** + * A helper that enables the "evaluation badge" feature in the editor. + */ + public readonly evaluationBadge: EvaluationBadge; + /** * A helper that manages the content of an `aria-live` regions used by editor features to announce status changes * to screen readers. @@ -137,6 +143,7 @@ export default abstract class EditorUI extends /* #__PURE__ */ ObservableMixin() this.focusTracker = new FocusTracker(); this.tooltipManager = new TooltipManager( editor ); this.poweredBy = new PoweredBy( editor ); + this.evaluationBadge = new EvaluationBadge( editor ); this.ariaLiveAnnouncer = new AriaLiveAnnouncer( editor ); this.set( 'viewportOffset', this._readViewportOffsetFromConfig() ); @@ -187,6 +194,7 @@ export default abstract class EditorUI extends /* #__PURE__ */ ObservableMixin() this.focusTracker.destroy(); this.tooltipManager.destroy( this.editor ); this.poweredBy.destroy(); + this.evaluationBadge.destroy(); // Clean–up the references to the CKEditor instance stored in the native editable DOM elements. for ( const domElement of this._editableElementsMap.values() ) { diff --git a/packages/ckeditor5-ui/src/editorui/evaluationbadge.ts b/packages/ckeditor5-ui/src/editorui/evaluationbadge.ts new file mode 100644 index 00000000000..032f4e8e975 --- /dev/null +++ b/packages/ckeditor5-ui/src/editorui/evaluationbadge.ts @@ -0,0 +1,115 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/** + * @module ui/editorui/evaluationbadge + */ + +import type { Editor } from '@ckeditor/ckeditor5-core'; +import { parseBase64EncodedObject, type Locale } from '@ckeditor/ckeditor5-utils'; + +import View from '../view.js'; +import Badge, { type BadgeConfig } from '../badge/badge.js'; + +/** + * A helper that enables the "evaluation badge" feature in the editor at the bottom of the editable element + * (editor root, source editing area, etc.) when the editor is focused. + * + * @private + */ +export default class EvaluationBadge extends Badge { + constructor( editor: Editor ) { + super( editor, { balloonClass: 'ck-evaluation-badge-balloon' } ); + } + + /** + * Enables "evaluation badge" label. + */ + protected override _isEnabled(): boolean { + const editor = this.editor; + const licenseKey = editor.config.get( 'licenseKey' )!; + const licenseType = getLicenseTypeFromLicenseKey( licenseKey ); + + return !!licenseType && [ 'trial', 'development' ].includes( licenseType ); + } + + /** + * Creates the content of the "evaluation badge". + */ + protected override _createBadgeContent(): View { + const licenseKey = this.editor.config.get( 'licenseKey' )!; + const licenseType = getLicenseTypeFromLicenseKey( licenseKey ); + const label = licenseType == 'trial' ? 'For evaluation purposes only' : 'For development purposes only'; + + return new EvaluationBadgeView( this.editor.locale, label ); + } + + /** + * Returns the normalized configuration for the "evaluation badge". + * It takes 'ui.poweredBy' configuration into account to determine the badge position and side. + */ + protected override _getNormalizedConfig(): BadgeConfig { + const badgeConfig = super._getNormalizedConfig(); + const userConfig = this.editor.config.get( 'ui.poweredBy' ) || {}; + const position = userConfig.position || badgeConfig.position; + const poweredBySide = userConfig.side || badgeConfig.side; + + return { + position, + side: poweredBySide === 'left' ? 'right' : 'left', + verticalOffset: badgeConfig.verticalOffset, + horizontalOffset: badgeConfig.horizontalOffset + }; + } +} + +/** + * A view displaying the "evaluation badge". + */ +class EvaluationBadgeView extends View { + /** + * Creates an instance of the "evaluation badge" view. + * + * @param locale The localization services instance. + * @param label The label text. + */ + constructor( locale: Locale, label: string ) { + super( locale ); + + this.setTemplate( { + tag: 'div', + attributes: { + class: [ 'ck', 'ck-evaluation-badge' ], + 'aria-hidden': true + }, + children: [ + { + tag: 'span', + attributes: { + class: [ 'ck', 'ck-evaluation-badge__label' ] + }, + children: [ label ] + } + ] + } ); + } +} + +/** + * Returns the license type based on the license key. + */ +function getLicenseTypeFromLicenseKey( licenseKey: string ): string | null { + if ( licenseKey == 'GPL' ) { + return 'GPL'; + } + + const licenseContent = parseBase64EncodedObject( licenseKey.split( '.' )[ 1 ] ); + + if ( !licenseContent ) { + return null; + } + + return licenseContent.licenseType || 'production'; +} diff --git a/packages/ckeditor5-ui/src/editorui/poweredby.ts b/packages/ckeditor5-ui/src/editorui/poweredby.ts index ef8b8d61bbc..cff7de48f78 100644 --- a/packages/ckeditor5-ui/src/editorui/poweredby.ts +++ b/packages/ckeditor5-ui/src/editorui/poweredby.ts @@ -61,14 +61,15 @@ export default class PoweredBy extends Badge { } /** - * TODO + * Creates a "powered by" badge content. */ protected override _createBadgeContent(): View { return new PoweredByView( this.editor.locale, this._getNormalizedConfig().label ); } /** - * TODO + * Returns the normalized configuration for the "powered by" badge. + * It takes the user configuration into account and falls back to the default one. */ protected override _getNormalizedConfig(): Required { const badgeConfig = super._getNormalizedConfig(); @@ -92,7 +93,7 @@ export default class PoweredBy extends Badge { */ class PoweredByView extends View { /** - * Created an instance of the "powered by" view. + * Creates an instance of the "powered by" view. * * @param locale The localization services instance. * @param label The label text. diff --git a/packages/ckeditor5-ui/theme/globals/_evaluationbadge.css b/packages/ckeditor5-ui/theme/globals/_evaluationbadge.css new file mode 100644 index 00000000000..a97e4d34dc8 --- /dev/null +++ b/packages/ckeditor5-ui/theme/globals/_evaluationbadge.css @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +:root { + --ck-evaluation-badge-line-height: 10px; + --ck-evaluation-badge-padding-vertical: 2px; + --ck-evaluation-badge-padding-horizontal: 4px; + --ck-evaluation-badge-text-color: hsl(0, 0%, 31%); + --ck-evaluation-badge-border-radius: var(--ck-border-radius); + --ck-evaluation-badge-background: hsl(0, 0%, 100%); + --ck-evaluation-badge-border-color: var(--ck-color-focus-border); +} + +.ck.ck-balloon-panel.ck-evaluation-badge-balloon { + --ck-border-radius: var(--ck-evaluation-badge-border-radius); + + box-shadow: none; + background: var(--ck-evaluation-badge-background); + min-height: unset; + z-index: calc( var(--ck-z-panel) - 1 ); + + & .ck.ck-evaluation-badge { + line-height: var(--ck-evaluation-badge-line-height); + + & .ck-evaluation-badge__label { + font-size: 7.5px; + letter-spacing: -.2px; + padding-left: 2px; + text-transform: uppercase; + font-weight: bold; + margin-right: 4px; + line-height: normal; + color: var(--ck-evaluation-badge-text-color); + } + } + + &[class*="position_inside"] { + border-color: transparent; + } + + &[class*="position_border"] { + border: var(--ck-focus-ring); + border-color: var(--ck-evaluation-badge-border-color); + } +} + diff --git a/packages/ckeditor5-ui/theme/globals/globals.css b/packages/ckeditor5-ui/theme/globals/globals.css index 68eaa1bef2a..01e9921a73f 100644 --- a/packages/ckeditor5-ui/theme/globals/globals.css +++ b/packages/ckeditor5-ui/theme/globals/globals.css @@ -7,3 +7,4 @@ @import "./_zindex.css"; @import "./_transition.css"; @import "./_poweredby.css"; +@import "./_evaluationbadge.css"; From dc7a14795ce861edde9cfe5bfba0c20c258b29c9 Mon Sep 17 00:00:00 2001 From: Marta Motyczynska Date: Thu, 27 Jun 2024 15:48:52 +0200 Subject: [PATCH 059/256] Extract generating license key for tests to util. --- .../tests/_utils-tests/generatelicensekey.js | 70 ++++++++++++++++ .../tests/_utils/generatelicensekey.js | 80 +++++++++++++++++++ .../tests/editor/licensecheck.js | 73 +---------------- .../ckeditor5-ui/tests/editorui/poweredby.js | 2 +- 4 files changed, 153 insertions(+), 72 deletions(-) create mode 100644 packages/ckeditor5-core/tests/_utils-tests/generatelicensekey.js create mode 100644 packages/ckeditor5-core/tests/_utils/generatelicensekey.js diff --git a/packages/ckeditor5-core/tests/_utils-tests/generatelicensekey.js b/packages/ckeditor5-core/tests/_utils-tests/generatelicensekey.js new file mode 100644 index 00000000000..04130933454 --- /dev/null +++ b/packages/ckeditor5-core/tests/_utils-tests/generatelicensekey.js @@ -0,0 +1,70 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/* globals atob */ + +import generateLicenseKey from '../_utils/generatelicensekey.js'; + +describe( 'generateLicenseKey util', () => { + describe( 'generateLicenseKey()', () => { + it( 'should generate a license key with custom properties', () => { + const { licenseKey } = generateLicenseKey( { + licensedHosts: [ 'example.com' ], + licenseType: 'trial', + usageEndpoint: 'https://example.com/usage', + distributionChannel: 'cdn', + whiteLabel: true + } ); + + const decodedPayload = JSON.parse( atob( licenseKey.split( '.' )[ 1 ] ) ); + + expect( decodedPayload.licensedHosts ).to.deep.equal( [ 'example.com' ] ); + expect( decodedPayload.licenseType ).to.equal( 'trial' ); + expect( decodedPayload.usageEndpoint ).to.equal( 'https://example.com/usage' ); + expect( decodedPayload.distributionChannel ).to.equal( 'cdn' ); + expect( decodedPayload.whiteLabel ).to.be.true; + } ); + + it( 'should generate a license key without header and tail', () => { + const { licenseKey } = generateLicenseKey( { + skipHeader: true, + skipTail: true + } ); + + expect( licenseKey.startsWith( 'foo.' ) ).to.be.false; + expect( licenseKey.endsWith( '.bar' ) ).to.be.false; + } ); + + it( 'should generate a license key with custom VC', () => { + const { licenseKey } = generateLicenseKey( { + customVc: 'abc123' + } ); + + const decodedPayload = JSON.parse( atob( licenseKey.split( '.' )[ 1 ] ) ); + + expect( decodedPayload.vc ).to.equal( 'abc123' ); + } ); + + it( 'should generate a license key with custom expiration date', () => { + const { licenseKey } = generateLicenseKey( { + isExpired: true + } ); + + const decodedPayload = JSON.parse( atob( licenseKey.split( '.' )[ 1 ] ) ); + + expect( decodedPayload.exp ).to.be.below( Date.now() / 1000 ); + } ); + + it( 'should generate a license key with custom jti', () => { + const { licenseKey } = generateLicenseKey( { + jtiExist: false + } ); + + const decodedPayload = JSON.parse( atob( licenseKey.split( '.' )[ 1 ] ) ); + + expect( decodedPayload.jti ).to.be.undefined; + } ); + } ); +} ); diff --git a/packages/ckeditor5-core/tests/_utils/generatelicensekey.js b/packages/ckeditor5-core/tests/_utils/generatelicensekey.js new file mode 100644 index 00000000000..820898cc24f --- /dev/null +++ b/packages/ckeditor5-core/tests/_utils/generatelicensekey.js @@ -0,0 +1,80 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/* globals btoa */ + +import { releaseDate, crc32 } from '@ckeditor/ckeditor5-utils'; + +/** + * Generates a license key for testing purposes. + */ +export default function generateKey( options = {} ) { + const { + isExpired = false, + jtiExist = true, + expExist = true, + vcExist = true, + customVc = undefined, + skipHeader = false, + skipTail = false, + daysAfterExpiration = 0 + } = options; + + const jti = 'foo'; + const releaseTimestamp = Date.parse( releaseDate ); + const day = 86400000; // one day in milliseconds. + + // Depending on isExpired parameter we are creating timestamp ten days + // before or after release day. + const expirationTimestamp = isExpired ? releaseTimestamp - 10 * day : releaseTimestamp + 10 * day; + const todayTimestamp = ( expirationTimestamp + daysAfterExpiration * day ); + + const payload = {}; + + [ 'licensedHosts', 'licenseType', 'usageEndpoint', 'distributionChannel', 'whiteLabel' ].forEach( prop => { + if ( prop in options ) { + payload[ prop ] = options[ prop ]; + } + } ); + + if ( jtiExist ) { + payload.jti = jti; + } + + if ( expExist ) { + payload.exp = Math.ceil( expirationTimestamp / 1000 ); + } + + if ( customVc ) { + payload.vc = customVc; + } else if ( vcExist ) { + const vc = crc32( getCrcInputData( payload ) ); + + payload.vc = vc; + } + + return { + licenseKey: `${ skipHeader ? '' : 'foo.' }${ encodePayload( payload ) }${ skipTail ? '' : '.bar' }`, + todayTimestamp + }; +} + +function encodePayload( claims ) { + return encodeBase64Safe( JSON.stringify( claims ) ); +} + +function encodeBase64Safe( text ) { + return btoa( text ).replace( /\+/g, '-' ).replace( /\//g, '_' ).replace( /=+$/, '' ); +} + +function getCrcInputData( licensePayload ) { + const keysToCheck = Object.getOwnPropertyNames( licensePayload ).sort(); + + const filteredValues = keysToCheck + .filter( key => key != 'vc' && licensePayload[ key ] != null ) + .map( key => licensePayload[ key ] ); + + return [ ...filteredValues ]; +} diff --git a/packages/ckeditor5-core/tests/editor/licensecheck.js b/packages/ckeditor5-core/tests/editor/licensecheck.js index f6e3a4b8716..571e5298c93 100644 --- a/packages/ckeditor5-core/tests/editor/licensecheck.js +++ b/packages/ckeditor5-core/tests/editor/licensecheck.js @@ -3,13 +3,13 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ -/* globals window, console, Response, globalThis, btoa */ +/* globals window, console, Response, globalThis */ -import { releaseDate, crc32 } from '@ckeditor/ckeditor5-utils'; import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror.js'; import { expectToThrowCKEditorError } from '@ckeditor/ckeditor5-utils/tests/_utils/utils.js'; import Editor from '../../src/editor/editor.js'; import testUtils from '../../tests/_utils/utils.js'; +import generateKey from '../_utils/generatelicensekey.js'; class TestEditor extends Editor { static create( config ) { @@ -640,72 +640,3 @@ function wait( time ) { window.setTimeout( res, time ); } ); } - -export function generateKey( options = {} ) { - const { - isExpired = false, - jtiExist = true, - expExist = true, - vcExist = true, - customVc = undefined, - skipHeader = false, - skipTail = false, - daysAfterExpiration = 0 - } = options; - - const jti = 'foo'; - const releaseTimestamp = Date.parse( releaseDate ); - const day = 86400000; // one day in milliseconds. - - // Depending on isExpired parameter we are creating timestamp ten days - // before or after release day. - const expirationTimestamp = isExpired ? releaseTimestamp - 10 * day : releaseTimestamp + 10 * day; - const todayTimestamp = ( expirationTimestamp + daysAfterExpiration * day ); - - const payload = {}; - - [ 'licensedHosts', 'licenseType', 'usageEndpoint', 'distributionChannel', 'whiteLabel' ].forEach( prop => { - if ( prop in options ) { - payload[ prop ] = options[ prop ]; - } - } ); - - if ( jtiExist ) { - payload.jti = jti; - } - - if ( expExist ) { - payload.exp = Math.ceil( expirationTimestamp / 1000 ); - } - - if ( customVc ) { - payload.vc = customVc; - } else if ( vcExist ) { - const vc = crc32( getCrcInputData( payload ) ); - - payload.vc = vc; - } - - return { - licenseKey: `${ skipHeader ? '' : 'foo.' }${ encodePayload( payload ) }${ skipTail ? '' : '.bar' }`, - todayTimestamp - }; -} - -function encodePayload( claims ) { - return encodeBase64Safe( JSON.stringify( claims ) ); -} - -function encodeBase64Safe( text ) { - return btoa( text ).replace( /\+/g, '-' ).replace( /\//g, '_' ).replace( /=+$/, '' ); -} - -function getCrcInputData( licensePayload ) { - const keysToCheck = Object.getOwnPropertyNames( licensePayload ).sort(); - - const filteredValues = keysToCheck - .filter( key => key != 'vc' && licensePayload[ key ] != null ) - .map( key => licensePayload[ key ] ); - - return [ ...filteredValues ]; -} diff --git a/packages/ckeditor5-ui/tests/editorui/poweredby.js b/packages/ckeditor5-ui/tests/editorui/poweredby.js index 563cb400eae..684f477b7ec 100644 --- a/packages/ckeditor5-ui/tests/editorui/poweredby.js +++ b/packages/ckeditor5-ui/tests/editorui/poweredby.js @@ -17,7 +17,7 @@ import { Rect, global } from '@ckeditor/ckeditor5-utils'; import SourceEditing from '@ckeditor/ckeditor5-source-editing/src/sourceediting.js'; import Heading from '@ckeditor/ckeditor5-heading/src/heading.js'; import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model.js'; -import { generateKey } from '@ckeditor/ckeditor5-core/tests/editor/licensecheck.js'; +import generateKey from '@ckeditor/ckeditor5-core/tests/_utils/generatelicensekey.js'; describe( 'PoweredBy', () => { let editor, element; From 56ab9b42afbcf00ac76741794eff7939d9b3b3ba Mon Sep 17 00:00:00 2001 From: Marta Motyczynska Date: Tue, 2 Jul 2024 17:37:14 +0200 Subject: [PATCH 060/256] Add tests for evaluation badge. Updated css. --- .../tests/editorui/evaluationbadge.js | 979 ++++++++++++++++++ .../ckeditor5-ui/tests/editorui/poweredby.js | 18 + .../theme/globals/_evaluationbadge.css | 1 + 3 files changed, 998 insertions(+) create mode 100644 packages/ckeditor5-ui/tests/editorui/evaluationbadge.js diff --git a/packages/ckeditor5-ui/tests/editorui/evaluationbadge.js b/packages/ckeditor5-ui/tests/editorui/evaluationbadge.js new file mode 100644 index 00000000000..05f433789f6 --- /dev/null +++ b/packages/ckeditor5-ui/tests/editorui/evaluationbadge.js @@ -0,0 +1,979 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/* global document, window, HTMLElement, getComputedStyle */ + +import { Editor } from '@ckeditor/ckeditor5-core'; +import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor.js'; +import EditorUI from '../../src/editorui/editorui.js'; +import { BalloonPanelView } from '../../src/index.js'; +import View from '../../src/view.js'; + +import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor.js'; +import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils.js'; +import { Rect, global } from '@ckeditor/ckeditor5-utils'; +import SourceEditing from '@ckeditor/ckeditor5-source-editing/src/sourceediting.js'; +import Heading from '@ckeditor/ckeditor5-heading/src/heading.js'; +import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model.js'; +import generateKey from '@ckeditor/ckeditor5-core/tests/_utils/generatelicensekey.js'; + +describe( 'EvaluationBadge', () => { + let editor, element, trialLicenseKey; + + testUtils.createSinonSandbox(); + + beforeEach( async () => { + trialLicenseKey = generateKey( { licenseType: 'trial' } ).licenseKey; + element = document.createElement( 'div' ); + document.body.appendChild( element ); + editor = await createEditor( element, { + plugins: [ SourceEditing ], + licenseKey: trialLicenseKey + } ); + + testUtils.sinon.stub( editor.editing.view.getDomRoot(), 'getBoundingClientRect' ).returns( { + top: 0, + left: 0, + right: 400, + width: 400, + bottom: 100, + height: 100 + } ); + + testUtils.sinon.stub( document.body, 'getBoundingClientRect' ).returns( { + top: 0, + right: 1000, + bottom: 1000, + left: 0, + width: 1000, + height: 1000 + } ); + + sinon.stub( global.window, 'innerWidth' ).value( 1000 ); + sinon.stub( global.window, 'innerHeight' ).value( 1000 ); + } ); + + afterEach( async () => { + element.remove(); + await editor.destroy(); + } ); + + describe( 'constructor()', () => { + describe( 'balloon creation', () => { + it( 'should not throw if there is no view in EditorUI', () => { + expect( () => { + const editor = new Editor( { licenseKey: trialLicenseKey } ); + + editor.model.document.createRoot(); + editor.ui = new EditorUI( editor ); + editor.editing.view.attachDomRoot( element ); + editor.fire( 'ready' ); + element.style.display = 'block'; + element.setAttribute( 'contenteditable', 'true' ); + editor.ui.focusTracker.add( element ); + element.focus(); + + editor.destroy(); + editor.ui.destroy(); + } ).to.not.throw(); + } ); + + it( 'should create the balloon on demand', () => { + expect( editor.ui.evaluationBadge._balloonView ).to.be.null; + + focusEditor( editor ); + + expect( editor.ui.evaluationBadge._balloonView ).to.be.instanceOf( BalloonPanelView ); + } ); + + it( 'should create the balloon when license type is `trial`', async () => { + const editor = await createEditor( element, { + licenseKey: trialLicenseKey + } ); + + expect( editor.ui.evaluationBadge._balloonView ).to.be.null; + + focusEditor( editor ); + + expect( editor.ui.evaluationBadge._balloonView ).to.be.instanceOf( BalloonPanelView ); + + const balloonElement = editor.ui.evaluationBadge._balloonView.element; + + expect( balloonElement.querySelector( '.ck-evaluation-badge__label' ).textContent ).to.equal( + 'For evaluation purposes only' + ); + + await editor.destroy(); + } ); + + it( 'should create the balloon when license type is `development`', async () => { + const devLicenseKey = generateKey( { licenseType: 'development' } ).licenseKey; + const editor = await createEditor( element, { + licenseKey: devLicenseKey + } ); + + expect( editor.ui.evaluationBadge._balloonView ).to.be.null; + + focusEditor( editor ); + + expect( editor.ui.evaluationBadge._balloonView ).to.be.instanceOf( BalloonPanelView ); + + const balloonElement = editor.ui.evaluationBadge._balloonView.element; + + expect( balloonElement.querySelector( '.ck-evaluation-badge__label' ).textContent ).to.equal( + 'For development purposes only' + ); + + await editor.destroy(); + } ); + + it( 'should not depend on white-label', async () => { + const { licenseKey } = generateKey( { whiteLabel: true, licenseType: 'trial' } ); + const editor = await createEditor( element, { + licenseKey + } ); + + expect( editor.ui.evaluationBadge._balloonView ).to.be.null; + + focusEditor( editor ); + + expect( editor.ui.evaluationBadge._balloonView ).to.be.instanceOf( BalloonPanelView ); + + await editor.destroy(); + } ); + } ); + + describe( 'balloon management on editor focus change', () => { + const originalGetVisible = Rect.prototype.getVisible; + + it( 'should show the balloon when the editor gets focused', () => { + focusEditor( editor ); + + expect( editor.ui.evaluationBadge._balloonView.isVisible ).to.be.true; + } ); + + it( 'should show the balloon if the focus is not in the editing root but in other editor UI', async () => { + const focusableEditorUIElement = document.createElement( 'input' ); + focusableEditorUIElement.type = 'text'; + document.body.appendChild( focusableEditorUIElement ); + + editor.ui.focusTracker.add( focusableEditorUIElement ); + + // Just generate the balloon on demand. + focusEditor( editor ); + blurEditor( editor ); + + await wait( 10 ); + const pinSpy = testUtils.sinon.spy( editor.ui.evaluationBadge._balloonView, 'pin' ); + + focusEditor( editor, focusableEditorUIElement ); + + sinon.assert.calledOnce( pinSpy ); + sinon.assert.calledWith( pinSpy, sinon.match.has( 'target', editor.editing.view.getDomRoot() ) ); + + focusableEditorUIElement.remove(); + } ); + + it( 'should hide the balloon on blur', async () => { + focusEditor( editor ); + + expect( editor.ui.evaluationBadge._balloonView.isVisible ).to.be.true; + + blurEditor( editor ); + + // FocusTracker's blur handler is asynchronous. + await wait( 200 ); + + expect( editor.ui.evaluationBadge._balloonView.isVisible ).to.be.false; + } ); + + // This is a weak test because it does not check the geometry but it will do. + it( 'should show the balloon when the source editing is engaged', async () => { + const domRoot = editor.editing.view.getDomRoot(); + const originalGetBoundingClientRect = HTMLElement.prototype.getBoundingClientRect; + + function isEditableElement( element ) { + return Array.from( editor.ui.getEditableElementsNames() ).map( name => { + return editor.ui.getEditableElement( name ); + } ).includes( element ); + } + + // Rect#getVisible() passthrough to ignore ancestors. Makes testing a lot easier. + testUtils.sinon.stub( Rect.prototype, 'getVisible' ).callsFake( function() { + if ( isEditableElement( this._source ) ) { + return new Rect( this._source ); + } else { + return originalGetVisible.call( this ); + } + } ); + + // Stub textarea's client rect. + testUtils.sinon.stub( HTMLElement.prototype, 'getBoundingClientRect' ).callsFake( function() { + if ( this.parentNode.classList.contains( 'ck-source-editing-area' ) ) { + return { + top: 0, + left: 0, + right: 400, + width: 400, + bottom: 200, + height: 200 + }; + } + + return originalGetBoundingClientRect.call( this ); + } ); + + focusEditor( editor ); + + domRoot.getBoundingClientRect.returns( { + top: 0, + left: 0, + right: 350, + width: 350, + bottom: 100, + height: 100 + } ); + + const pinSpy = testUtils.sinon.spy( editor.ui.evaluationBadge._balloonView, 'pin' ); + + editor.ui.fire( 'update' ); + + await wait( 75 ); + + expect( editor.ui.evaluationBadge._balloonView.isVisible ).to.be.true; + expect( editor.ui.evaluationBadge._balloonView.position ).to.equal( 'position_border-side_left' ); + sinon.assert.calledWith( pinSpy.lastCall, sinon.match.has( 'target', domRoot ) ); + + editor.plugins.get( 'SourceEditing' ).isSourceEditingMode = true; + + const sourceAreaElement = editor.ui.getEditableElement( 'sourceEditing:main' ); + + focusEditor( editor, sourceAreaElement ); + sinon.assert.calledWith( + pinSpy.lastCall, + sinon.match.has( 'target', sourceAreaElement ) + ); + + expect( editor.ui.evaluationBadge._balloonView.isVisible ).to.be.true; + expect( editor.ui.evaluationBadge._balloonView.position ).to.equal( 'position_border-side_left' ); + + editor.plugins.get( 'SourceEditing' ).isSourceEditingMode = false; + focusEditor( editor ); + + expect( editor.ui.evaluationBadge._balloonView.isVisible ).to.be.true; + expect( editor.ui.evaluationBadge._balloonView.position ).to.equal( 'position_border-side_left' ); + sinon.assert.calledWith( pinSpy.lastCall, sinon.match.has( 'target', domRoot ) ); + } ); + } ); + + describe( 'balloon management on EditorUI#update', () => { + it( 'should not trigger if the editor is not focused', () => { + expect( editor.ui.evaluationBadge._balloonView ).to.be.null; + + editor.ui.fire( 'update' ); + + expect( editor.ui.evaluationBadge._balloonView ).to.be.null; + } ); + + it( 'should (re-)show the balloon but throttled', async () => { + focusEditor( editor ); + + const pinSpy = testUtils.sinon.spy( editor.ui.evaluationBadge._balloonView, 'pin' ); + + editor.ui.fire( 'update' ); + editor.ui.fire( 'update' ); + + sinon.assert.notCalled( pinSpy ); + + await wait( 75 ); + + sinon.assert.calledOnce( pinSpy ); + sinon.assert.calledWith( pinSpy.firstCall, sinon.match.has( 'target', editor.editing.view.getDomRoot() ) ); + } ); + + it( 'should (re-)show the balloon if the focus is not in the editing root but in other editor UI', async () => { + const focusableEditorUIElement = document.createElement( 'input' ); + focusableEditorUIElement.type = 'text'; + editor.ui.focusTracker.add( focusableEditorUIElement ); + document.body.appendChild( focusableEditorUIElement ); + + focusEditor( editor, focusableEditorUIElement ); + + const pinSpy = testUtils.sinon.spy( editor.ui.evaluationBadge._balloonView, 'pin' ); + + sinon.assert.notCalled( pinSpy ); + + editor.ui.fire( 'update' ); + editor.ui.fire( 'update' ); + + sinon.assert.calledOnce( pinSpy ); + + await wait( 75 ); + + sinon.assert.calledTwice( pinSpy ); + sinon.assert.calledWith( pinSpy, sinon.match.has( 'target', editor.editing.view.getDomRoot() ) ); + focusableEditorUIElement.remove(); + } ); + } ); + + describe( 'balloon view', () => { + let balloon, focusTrackerAddSpy; + + beforeEach( () => { + focusTrackerAddSpy = testUtils.sinon.spy( editor.ui.focusTracker, 'add' ); + + focusEditor( editor ); + + balloon = editor.ui.evaluationBadge._balloonView; + } ); + + it( 'should be an instance of BalloonPanelView', () => { + expect( balloon ).to.be.instanceOf( BalloonPanelView ); + } ); + + it( 'should host an evaluation badge view', () => { + expect( balloon.content.first ).to.be.instanceOf( View ); + } ); + + it( 'should have no arrow', () => { + expect( balloon.withArrow ).to.be.false; + } ); + + it( 'should have a specific CSS class', () => { + expect( balloon.class ).to.equal( 'ck-evaluation-badge-balloon' ); + } ); + + it( 'should be added to editor\'s body view collection', () => { + expect( editor.ui.view.body.has( balloon ) ).to.be.true; + } ); + + it( 'should be registered in the focus tracker to avoid focus loss on click', () => { + sinon.assert.calledWith( focusTrackerAddSpy, balloon.element ); + } ); + } ); + + describe( 'evaluation badge view', () => { + let view; + + beforeEach( () => { + focusEditor( editor ); + + view = editor.ui.evaluationBadge._balloonView.content.first; + } ); + + it( 'should have specific CSS classes', () => { + expect( view.element.classList.contains( 'ck' ) ).to.be.true; + expect( view.element.classList.contains( 'ck-evaluation-badge' ) ).to.be.true; + } ); + + it( 'should be excluded from the accessibility tree', () => { + expect( view.element.getAttribute( 'aria-hidden' ) ).to.equal( 'true' ); + } ); + + it( 'should not be accessible via tab key navigation', () => { + expect( view.element.firstChild.tabIndex ).to.equal( -1 ); + } ); + } ); + } ); + + describe( 'destroy()', () => { + describe( 'if there was a balloon', () => { + beforeEach( () => { + focusEditor( editor ); + } ); + + it( 'should unpin the balloon', () => { + const unpinSpy = testUtils.sinon.spy( editor.ui.evaluationBadge._balloonView, 'unpin' ); + + editor.destroy(); + + sinon.assert.calledOnce( unpinSpy ); + } ); + + it( 'should destroy the balloon', () => { + const destroySpy = testUtils.sinon.spy( editor.ui.evaluationBadge._balloonView, 'destroy' ); + + editor.destroy(); + + sinon.assert.called( destroySpy ); + + expect( editor.ui.evaluationBadge._balloonView ).to.be.null; + } ); + + it( 'should cancel any throttled show to avoid post-destroy timed errors', () => { + const spy = testUtils.sinon.spy( editor.ui.evaluationBadge._showBalloonThrottled, 'cancel' ); + + editor.destroy(); + + sinon.assert.calledOnce( spy ); + } ); + } ); + + describe( 'if there was no balloon', () => { + it( 'should not throw', () => { + expect( () => { + editor.destroy(); + } ).to.not.throw(); + } ); + } ); + + it( 'should destroy the emitter listeners', () => { + const spy = testUtils.sinon.spy( editor.ui.evaluationBadge, 'stopListening' ); + + editor.destroy(); + + sinon.assert.calledOnce( spy ); + } ); + } ); + + describe( 'balloon positioning depending on environment and configuration', () => { + const originalGetVisible = Rect.prototype.getVisible; + let rootRect, balloonRect; + + beforeEach( () => { + rootRect = new Rect( { top: 0, left: 0, width: 400, right: 400, bottom: 100, height: 100 } ); + balloonRect = new Rect( { top: 0, left: 0, width: 20, right: 20, bottom: 10, height: 10 } ); + } ); + + it( 'should not show the balloon if the root is not visible vertically', async () => { + const domRoot = editor.editing.view.getDomRoot(); + const parentWithOverflow = document.createElement( 'div' ); + + parentWithOverflow.style.overflow = 'scroll'; + // Is not enough height to be visible vertically. + parentWithOverflow.style.height = '99px'; + + document.body.appendChild( parentWithOverflow ); + parentWithOverflow.appendChild( domRoot ); + + focusEditor( editor ); + + expect( editor.ui.evaluationBadge._balloonView.isVisible ).to.be.true; + expect( editor.ui.evaluationBadge._balloonView.position ).to.equal( 'arrowless' ); + + parentWithOverflow.remove(); + } ); + + it( 'should not show the balloon if the root is not visible horizontally', async () => { + const domRoot = editor.editing.view.getDomRoot(); + const parentWithOverflow = document.createElement( 'div' ); + + parentWithOverflow.style.overflow = 'scroll'; + // Is not enough width to be visible horizontally. + parentWithOverflow.style.width = '399px'; + + document.body.appendChild( parentWithOverflow ); + parentWithOverflow.appendChild( domRoot ); + + focusEditor( editor ); + + expect( editor.ui.evaluationBadge._balloonView.isVisible ).to.be.true; + expect( editor.ui.evaluationBadge._balloonView.position ).to.equal( 'arrowless' ); + + parentWithOverflow.remove(); + } ); + + it( 'should position the badge to the left right if the UI language is RTL (and powered-by is on the left)', async () => { + const editor = await createEditor( element, { + language: 'ar', + licenseKey: trialLicenseKey + } ); + + testUtils.sinon.stub( editor.ui.getEditableElement( 'main' ), 'getBoundingClientRect' ).returns( { + top: 0, + left: 0, + right: 400, + width: 400, + bottom: 100, + height: 100 + } ); + + focusEditor( editor ); + + const pinSpy = testUtils.sinon.spy( editor.ui.evaluationBadge._balloonView, 'pin' ); + + editor.ui.fire( 'update' ); + + // Throttled #update listener. + await wait( 75 ); + + sinon.assert.calledOnce( pinSpy ); + + const pinArgs = pinSpy.firstCall.args[ 0 ]; + const positioningFunction = pinArgs.positions[ 0 ]; + + expect( pinArgs.target ).to.equal( editor.editing.view.getDomRoot() ); + expect( positioningFunction( rootRect, balloonRect ) ).to.deep.equal( { + top: 95, + left: 375, + name: 'position_border-side_right', + config: { + withArrow: false + } + } ); + + await editor.destroy(); + } ); + + it( 'should position the balloon in the lower left corner by default', async () => { + focusEditor( editor ); + + const pinSpy = testUtils.sinon.spy( editor.ui.evaluationBadge._balloonView, 'pin' ); + + editor.ui.fire( 'update' ); + + // Throttled #update listener. + await wait( 75 ); + + sinon.assert.calledOnce( pinSpy ); + + const pinArgs = pinSpy.firstCall.args[ 0 ]; + const positioningFunction = pinArgs.positions[ 0 ]; + + expect( pinArgs.target ).to.equal( editor.editing.view.getDomRoot() ); + expect( positioningFunction( rootRect, balloonRect ) ).to.deep.equal( { + top: 95, + left: 5, + name: 'position_border-side_left', + config: { + withArrow: false + } + } ); + } ); + + it( 'should position the balloon in the lower right corner if poweredby is configured on the left', async () => { + const editor = await createEditor( element, { + ui: { + poweredBy: { + side: 'left' + } + }, + licenseKey: trialLicenseKey + } ); + + testUtils.sinon.stub( editor.ui.getEditableElement( 'main' ), 'getBoundingClientRect' ).returns( { + top: 0, + left: 0, + right: 400, + width: 400, + bottom: 100, + height: 100 + } ); + + focusEditor( editor ); + + const pinSpy = testUtils.sinon.spy( editor.ui.evaluationBadge._balloonView, 'pin' ); + + editor.ui.fire( 'update' ); + + // Throttled #update listener. + await wait( 75 ); + + sinon.assert.calledOnce( pinSpy ); + + const pinArgs = pinSpy.firstCall.args[ 0 ]; + const positioningFunction = pinArgs.positions[ 0 ]; + + expect( pinArgs.target ).to.equal( editor.editing.view.getDomRoot() ); + expect( positioningFunction( rootRect, balloonRect ) ).to.deep.equal( { + top: 95, + left: 375, + name: 'position_border-side_right', + config: { + withArrow: false + } + } ); + + await editor.destroy(); + } ); + + it( 'should position the balloon over the bottom root border if configured', async () => { + const editor = await createEditor( element, { + ui: { + poweredBy: { + position: 'border' + } + }, + licenseKey: trialLicenseKey + } ); + + testUtils.sinon.stub( editor.ui.getEditableElement( 'main' ), 'getBoundingClientRect' ).returns( { + top: 0, + left: 0, + right: 400, + width: 400, + bottom: 100, + height: 100 + } ); + + focusEditor( editor ); + + const pinSpy = testUtils.sinon.spy( editor.ui.evaluationBadge._balloonView, 'pin' ); + + editor.ui.fire( 'update' ); + + // Throttled #update listener. + await wait( 75 ); + + sinon.assert.calledOnce( pinSpy ); + + const pinArgs = pinSpy.firstCall.args[ 0 ]; + const positioningFunction = pinArgs.positions[ 0 ]; + + expect( pinArgs.target ).to.equal( editor.editing.view.getDomRoot() ); + expect( positioningFunction( rootRect, balloonRect ) ).to.deep.equal( { + top: 95, + left: 5, + name: 'position_border-side_left', + config: { + withArrow: false + } + } ); + + await editor.destroy(); + } ); + + it( 'should position the balloon in the corner of the root if configured', async () => { + const editor = await createEditor( element, { + ui: { + poweredBy: { + position: 'inside' + } + }, + licenseKey: trialLicenseKey + } ); + + testUtils.sinon.stub( editor.ui.getEditableElement( 'main' ), 'getBoundingClientRect' ).returns( { + top: 0, + left: 0, + right: 400, + width: 400, + bottom: 100, + height: 100 + } ); + + focusEditor( editor ); + + const pinSpy = testUtils.sinon.spy( editor.ui.evaluationBadge._balloonView, 'pin' ); + + editor.ui.fire( 'update' ); + + // Throttled #update listener. + await wait( 75 ); + + sinon.assert.calledOnce( pinSpy ); + + const pinArgs = pinSpy.firstCall.args[ 0 ]; + const positioningFunction = pinArgs.positions[ 0 ]; + + expect( pinArgs.target ).to.equal( editor.editing.view.getDomRoot() ); + expect( positioningFunction( rootRect, balloonRect ) ).to.deep.equal( { + top: 90, + left: 5, + name: 'position_inside-side_left', + config: { + withArrow: false + } + } ); + + await editor.destroy(); + } ); + + it( 'should hide the balloon if displayed over the bottom root border but partially cropped by an ancestor', async () => { + const editor = await createEditor( element, { + ui: { + poweredBy: { + position: 'border' + } + }, + licenseKey: trialLicenseKey + } ); + + const domRoot = editor.editing.view.getDomRoot(); + + rootRect = new Rect( { top: 0, left: 0, width: 100, right: 100, bottom: 10, height: 10 } ); + + focusEditor( editor ); + + const pinSpy = testUtils.sinon.spy( editor.ui.evaluationBadge._balloonView, 'pin' ); + + editor.ui.fire( 'update' ); + + // Throttled #update listener. + await wait( 75 ); + + sinon.assert.calledOnce( pinSpy ); + + const pinArgs = pinSpy.firstCall.args[ 0 ]; + const positioningFunction = pinArgs.positions[ 0 ]; + + expect( pinArgs.target ).to.equal( domRoot ); + expect( positioningFunction( rootRect, balloonRect ) ).to.equal( null ); + + await editor.destroy(); + } ); + + it( 'should hide the balloon if displayed in the corner of the root but partially cropped by an ancestor', async () => { + const editor = await createEditor( element, { + ui: { + poweredBy: { + position: 'inside' + } + }, + licenseKey: trialLicenseKey + } ); + + rootRect = new Rect( { top: 0, left: 0, width: 400, right: 400, bottom: 200, height: 200 } ); + + testUtils.sinon.stub( rootRect, 'getVisible' ).returns( { top: 0, left: 0, width: 400, right: 400, bottom: 10, height: 10 } ); + + balloonRect = new Rect( { top: 200, left: 0, width: 20, right: 20, bottom: 210, height: 10 } ); + + const domRoot = editor.editing.view.getDomRoot(); + + focusEditor( editor ); + + const pinSpy = testUtils.sinon.spy( editor.ui.evaluationBadge._balloonView, 'pin' ); + + editor.ui.fire( 'update' ); + + // Throttled #update listener. + await wait( 75 ); + + sinon.assert.calledOnce( pinSpy ); + + const pinArgs = pinSpy.firstCall.args[ 0 ]; + const positioningFunction = pinArgs.positions[ 0 ]; + + expect( pinArgs.target ).to.equal( domRoot ); + expect( positioningFunction( rootRect, balloonRect ) ).to.equal( null ); + + await editor.destroy(); + } ); + + it( 'should not display the balloon if the root is narrower than 350px', async () => { + const domRoot = editor.editing.view.getDomRoot(); + + testUtils.sinon.stub( Rect.prototype, 'getVisible' ).callsFake( function() { + if ( this._source === domRoot ) { + return new Rect( domRoot ); + } else { + return originalGetVisible.call( this ); + } + } ); + + domRoot.getBoundingClientRect.returns( { + top: 0, + left: 0, + right: 349, + width: 349, + bottom: 100, + height: 100 + } ); + + focusEditor( editor ); + + editor.ui.fire( 'update' ); + + // Throttled #update listener. + await wait( 75 ); + + const pinSpy = testUtils.sinon.spy( editor.ui.evaluationBadge._balloonView, 'pin' ); + + expect( editor.ui.evaluationBadge._balloonView.isVisible ).to.be.true; + expect( editor.ui.evaluationBadge._balloonView.position ).to.equal( 'arrowless' ); + + domRoot.getBoundingClientRect.returns( { + top: 0, + left: 0, + right: 350, + width: 350, + bottom: 100, + height: 100 + } ); + + editor.ui.fire( 'update' ); + + // Throttled #update listener. + await wait( 75 ); + + expect( editor.ui.evaluationBadge._balloonView.isVisible ).to.be.true; + expect( editor.ui.evaluationBadge._balloonView.position ).to.equal( 'position_border-side_left' ); + + const pinArgs = pinSpy.firstCall.args[ 0 ]; + const positioningFunction = pinArgs.positions[ 0 ]; + + expect( pinArgs.target ).to.equal( editor.editing.view.getDomRoot() ); + expect( positioningFunction( rootRect, balloonRect ) ).to.deep.equal( { + top: 95, + left: 5, + name: 'position_border-side_left', + config: { + withArrow: false + } + } ); + } ); + + it( 'should not display the balloon if the root is shorter than 50px', async () => { + const domRoot = editor.editing.view.getDomRoot(); + + testUtils.sinon.stub( Rect.prototype, 'getVisible' ).callsFake( function() { + if ( this._source === domRoot ) { + return new Rect( domRoot ); + } else { + return originalGetVisible.call( this ); + } + } ); + + domRoot.getBoundingClientRect.returns( { + top: 0, + left: 0, + right: 1000, + width: 1000, + bottom: 49, + height: 49 + } ); + + focusEditor( editor ); + + editor.ui.fire( 'update' ); + + // Throttled #update listener. + await wait( 75 ); + + const pinSpy = testUtils.sinon.spy( editor.ui.evaluationBadge._balloonView, 'pin' ); + + expect( editor.ui.evaluationBadge._balloonView.isVisible ).to.be.true; + expect( editor.ui.evaluationBadge._balloonView.position ).to.equal( 'arrowless' ); + + domRoot.getBoundingClientRect.returns( { + top: 0, + left: 0, + right: 1000, + width: 1000, + bottom: 50, + height: 50 + } ); + + editor.ui.fire( 'update' ); + + // Throttled #update listener. + await wait( 75 ); + + expect( editor.ui.evaluationBadge._balloonView.isVisible ).to.be.true; + expect( editor.ui.evaluationBadge._balloonView.position ).to.equal( 'position_border-side_left' ); + + const pinArgs = pinSpy.firstCall.args[ 0 ]; + const positioningFunction = pinArgs.positions[ 0 ]; + + expect( pinArgs.target ).to.equal( editor.editing.view.getDomRoot() ); + expect( positioningFunction( rootRect, balloonRect ) ).to.deep.equal( { + top: 45, + left: 5, + name: 'position_border-side_left', + config: { + withArrow: false + } + } ); + } ); + } ); + + it( 'should have the z-index lower than a regular BalloonPanelView instance', () => { + focusEditor( editor ); + + const balloonView = new BalloonPanelView(); + balloonView.render(); + + const zIndexOfEvaluationBadgeBalloon = Number( getComputedStyle( editor.ui.evaluationBadge._balloonView.element ).zIndex ); + + document.body.appendChild( balloonView.element ); + + const zIndexOfRegularBalloon = Number( getComputedStyle( balloonView.element ).zIndex ); + + expect( zIndexOfEvaluationBadgeBalloon ).to.be.lessThan( zIndexOfRegularBalloon ); + + balloonView.element.remove(); + balloonView.destroy(); + } ); + + it( 'should not overlap a dropdown panel in a toolbar', async () => { + const editor = await createClassicEditor( element, { + toolbar: [ 'heading' ], + plugins: [ Heading ], + ui: { + poweredBy: { + position: 'inside' + } + }, + licenseKey: trialLicenseKey + } ); + + setData( editor.model, 'foo[]bar' ); + + focusEditor( editor ); + + const headingToolbarButton = editor.ui.view.toolbar.items + .find( item => item.buttonView && item.buttonView.label.startsWith( 'Heading' ) ); + + const evaluationBadgeElement = editor.ui.evaluationBadge._balloonView.element; + + const evaluationBadgeElementGeometry = new Rect( evaluationBadgeElement ); + + const middleOfTheEvaluationBadgeCoords = { + x: ( evaluationBadgeElementGeometry.width / 2 ) + evaluationBadgeElementGeometry.left, + y: ( evaluationBadgeElementGeometry.height / 2 ) + evaluationBadgeElementGeometry.top + }; + + let elementFromPoint = document.elementFromPoint( + middleOfTheEvaluationBadgeCoords.x, + middleOfTheEvaluationBadgeCoords.y + ); + + expect( elementFromPoint.classList.contains( 'ck-evaluation-badge__label' ) ).to.be.true; + + // show heading dropdown + headingToolbarButton.buttonView.fire( 'execute' ); + + elementFromPoint = document.elementFromPoint( + middleOfTheEvaluationBadgeCoords.x, + middleOfTheEvaluationBadgeCoords.y + ); + + expect( elementFromPoint.classList.contains( 'ck-button__label' ) ).to.be.true; + + await editor.destroy(); + } ); + + async function createEditor( element, config = {} ) { + return ClassicTestEditor.create( element, config ); + } + + async function createClassicEditor( element, config = {} ) { + return ClassicEditor.create( element, config ); + } + + function wait( time ) { + return new Promise( res => { + window.setTimeout( res, time ); + } ); + } + + function focusEditor( editor, focusableUIElement ) { + if ( !focusableUIElement ) { + focusableUIElement = editor.editing.view.getDomRoot(); + editor.editing.view.focus(); + } else { + focusableUIElement.focus(); + } + + editor.ui.focusTracker.focusedElement = focusableUIElement; + editor.ui.focusTracker.isFocused = true; + } + + function blurEditor( editor ) { + editor.ui.focusTracker.focusedElement = null; + editor.ui.focusTracker.isFocused = null; + } +} ); diff --git a/packages/ckeditor5-ui/tests/editorui/poweredby.js b/packages/ckeditor5-ui/tests/editorui/poweredby.js index 684f477b7ec..62c4aa05184 100644 --- a/packages/ckeditor5-ui/tests/editorui/poweredby.js +++ b/packages/ckeditor5-ui/tests/editorui/poweredby.js @@ -98,6 +98,24 @@ describe( 'PoweredBy', () => { await editor.destroy(); } ); + it( 'should create the balloon when license is invalid', async () => { + const showErrorStub = sinon.stub( ClassicTestEditor.prototype, '_showLicenseError' ); + + const editor = await createEditor( element, { + licenseKey: 'foo.bar.baz' + } ); + + expect( editor.ui.poweredBy._balloonView ).to.be.null; + + focusEditor( editor ); + + expect( editor.ui.poweredBy._balloonView ).to.be.instanceOf( BalloonPanelView ); + + await editor.destroy(); + + showErrorStub.restore(); + } ); + it( 'should not create the balloon when a white-label license key is configured', async () => { const { licenseKey } = generateKey( { whiteLabel: true } ); const editor = await createEditor( element, { diff --git a/packages/ckeditor5-ui/theme/globals/_evaluationbadge.css b/packages/ckeditor5-ui/theme/globals/_evaluationbadge.css index a97e4d34dc8..3ab6e3fcb86 100644 --- a/packages/ckeditor5-ui/theme/globals/_evaluationbadge.css +++ b/packages/ckeditor5-ui/theme/globals/_evaluationbadge.css @@ -23,6 +23,7 @@ & .ck.ck-evaluation-badge { line-height: var(--ck-evaluation-badge-line-height); + padding: var(--ck-powered-by-padding-vertical) var(--ck-powered-by-padding-horizontal); & .ck-evaluation-badge__label { font-size: 7.5px; From 6fcc3508f7bc5bd2a76bab2ad0d5be8882038503 Mon Sep 17 00:00:00 2001 From: Marta Motyczynska Date: Wed, 3 Jul 2024 12:46:33 +0200 Subject: [PATCH 061/256] Tests for evaluation badge in editorui. --- packages/ckeditor5-ui/tests/editorui/editorui.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/ckeditor5-ui/tests/editorui/editorui.js b/packages/ckeditor5-ui/tests/editorui/editorui.js index 035587c24d8..71157e3a469 100644 --- a/packages/ckeditor5-ui/tests/editorui/editorui.js +++ b/packages/ckeditor5-ui/tests/editorui/editorui.js @@ -9,6 +9,7 @@ import ComponentFactory from '../../src/componentfactory.js'; import ToolbarView from '../../src/toolbar/toolbarview.js'; import TooltipManager from '../../src/tooltipmanager.js'; import PoweredBy from '../../src/editorui/poweredby.js'; +import EvaluationBadge from '../../src/editorui/evaluationbadge.js'; import AriaLiveAnnouncer from '../../src/arialiveannouncer.js'; import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker.js'; import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard.js'; @@ -55,6 +56,10 @@ describe( 'EditorUI', () => { expect( ui.poweredBy ).to.be.instanceOf( PoweredBy ); } ); + it( 'should create #evaluationBadge', () => { + expect( ui.evaluationBadge ).to.be.instanceOf( EvaluationBadge ); + } ); + it( 'should create the aria live announcer instance', () => { expect( ui.ariaLiveAnnouncer ).to.be.instanceOf( AriaLiveAnnouncer ); } ); @@ -179,6 +184,14 @@ describe( 'EditorUI', () => { sinon.assert.calledOnce( destroySpy ); } ); + + it( 'should destroy #evaluationBadge', () => { + const destroySpy = sinon.spy( ui.evaluationBadge, 'destroy' ); + + ui.destroy(); + + sinon.assert.calledOnce( destroySpy ); + } ); } ); describe( 'setEditableElement()', () => { From f1c769a9d19c141198b2d505eb8630905e25da75 Mon Sep 17 00:00:00 2001 From: Marta Motyczynska Date: Wed, 3 Jul 2024 13:52:27 +0200 Subject: [PATCH 062/256] Tests for Badge. --- packages/ckeditor5-ui/tests/badge/badge.js | 666 +++++++++++++++++++++ 1 file changed, 666 insertions(+) create mode 100644 packages/ckeditor5-ui/tests/badge/badge.js diff --git a/packages/ckeditor5-ui/tests/badge/badge.js b/packages/ckeditor5-ui/tests/badge/badge.js new file mode 100644 index 00000000000..a5a82891a3c --- /dev/null +++ b/packages/ckeditor5-ui/tests/badge/badge.js @@ -0,0 +1,666 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/* global document, window, HTMLElement */ + +import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor.js'; +import SourceEditing from '@ckeditor/ckeditor5-source-editing/src/sourceediting.js'; +import { Rect, global } from '@ckeditor/ckeditor5-utils'; +import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils.js'; + +import { BalloonPanelView } from '../../src/index.js'; +import View from '../../src/view.js'; +import Badge from '../../src/badge/badge.js'; + +class BadgeExtended extends Badge { + _isEnabled() { + return true; + } + + _createBadgeContent() { + return new EvaluationBadgeView( this.editor.locale, 'Badge extended label' ); + } +} + +class EvaluationBadgeView extends View { + constructor( locale, label ) { + super( locale ); + + this.setTemplate( { + tag: 'div', + attributes: { + class: [ 'ck-badge-extended' ] + }, + children: [ + { + tag: 'span', + attributes: { + class: [ 'ck-badge-extended__label' ] + }, + children: [ label ] + } + ] + } ); + } +} + +describe( 'Badge', () => { + let editor, element, badge; + + testUtils.createSinonSandbox(); + + beforeEach( async () => { + element = document.createElement( 'div' ); + document.body.appendChild( element ); + editor = await createEditor( element ); + + badge = new BadgeExtended( editor ); + editor.fire( 'ready' ); + + testUtils.sinon.stub( editor.editing.view.getDomRoot(), 'getBoundingClientRect' ).returns( { + top: 0, + left: 0, + right: 400, + width: 400, + bottom: 100, + height: 100 + } ); + + testUtils.sinon.stub( document.body, 'getBoundingClientRect' ).returns( { + top: 0, + right: 1000, + bottom: 1000, + left: 0, + width: 1000, + height: 1000 + } ); + + sinon.stub( global.window, 'innerWidth' ).value( 1000 ); + sinon.stub( global.window, 'innerHeight' ).value( 1000 ); + } ); + + afterEach( async () => { + element.remove(); + await editor.destroy(); + } ); + + describe( 'constructor()', () => { + describe( 'balloon creation', () => { + it( 'should create the balloon on demand', () => { + expect( badge._balloonView ).to.be.null; + + focusEditor( editor ); + + expect( badge._balloonView ).to.be.instanceOf( BalloonPanelView ); + } ); + } ); + + describe( 'balloon management on editor focus change', () => { + const originalGetVisible = Rect.prototype.getVisible; + + it( 'should show the balloon when the editor gets focused', () => { + focusEditor( editor ); + + expect( badge._balloonView.isVisible ).to.be.true; + } ); + + it( 'should show the balloon if the focus is not in the editing root but in other editor UI', async () => { + const focusableEditorUIElement = document.createElement( 'input' ); + focusableEditorUIElement.type = 'text'; + document.body.appendChild( focusableEditorUIElement ); + + editor.ui.focusTracker.add( focusableEditorUIElement ); + + // Just generate the balloon on demand. + focusEditor( editor ); + blurEditor( editor ); + + await wait( 10 ); + const pinSpy = testUtils.sinon.spy( badge._balloonView, 'pin' ); + + focusEditor( editor, focusableEditorUIElement ); + + sinon.assert.calledOnce( pinSpy ); + sinon.assert.calledWith( pinSpy, sinon.match.has( 'target', editor.editing.view.getDomRoot() ) ); + + focusableEditorUIElement.remove(); + } ); + + it( 'should hide the balloon on blur', async () => { + focusEditor( editor ); + + expect( badge._balloonView.isVisible ).to.be.true; + + blurEditor( editor ); + + // FocusTracker's blur handler is asynchronous. + await wait( 200 ); + + expect( badge._balloonView.isVisible ).to.be.false; + } ); + + // This is a weak test because it does not check the geometry but it will do. + it( 'should show the balloon when the source editing is engaged', async () => { + const domRoot = editor.editing.view.getDomRoot(); + const originalGetBoundingClientRect = HTMLElement.prototype.getBoundingClientRect; + + function isEditableElement( element ) { + return Array.from( editor.ui.getEditableElementsNames() ).map( name => { + return editor.ui.getEditableElement( name ); + } ).includes( element ); + } + + // Rect#getVisible() passthrough to ignore ancestors. Makes testing a lot easier. + testUtils.sinon.stub( Rect.prototype, 'getVisible' ).callsFake( function() { + if ( isEditableElement( this._source ) ) { + return new Rect( this._source ); + } else { + return originalGetVisible.call( this ); + } + } ); + + // Stub textarea's client rect. + testUtils.sinon.stub( HTMLElement.prototype, 'getBoundingClientRect' ).callsFake( function() { + if ( this.parentNode.classList.contains( 'ck-source-editing-area' ) ) { + return { + top: 0, + left: 0, + right: 400, + width: 400, + bottom: 200, + height: 200 + }; + } + + return originalGetBoundingClientRect.call( this ); + } ); + + focusEditor( editor ); + + domRoot.getBoundingClientRect.returns( { + top: 0, + left: 0, + right: 350, + width: 350, + bottom: 100, + height: 100 + } ); + + const pinSpy = testUtils.sinon.spy( badge._balloonView, 'pin' ); + + editor.ui.fire( 'update' ); + + await wait( 75 ); + + expect( badge._balloonView.isVisible ).to.be.true; + expect( badge._balloonView.position ).to.equal( 'position_border-side_right' ); + sinon.assert.calledWith( pinSpy.lastCall, sinon.match.has( 'target', domRoot ) ); + + editor.plugins.get( 'SourceEditing' ).isSourceEditingMode = true; + + const sourceAreaElement = editor.ui.getEditableElement( 'sourceEditing:main' ); + + focusEditor( editor, sourceAreaElement ); + sinon.assert.calledWith( + pinSpy.lastCall, + sinon.match.has( 'target', sourceAreaElement ) + ); + + expect( badge._balloonView.isVisible ).to.be.true; + expect( badge._balloonView.position ).to.equal( 'position_border-side_right' ); + + editor.plugins.get( 'SourceEditing' ).isSourceEditingMode = false; + focusEditor( editor ); + + expect( badge._balloonView.isVisible ).to.be.true; + expect( badge._balloonView.position ).to.equal( 'position_border-side_right' ); + sinon.assert.calledWith( pinSpy.lastCall, sinon.match.has( 'target', domRoot ) ); + } ); + } ); + + describe( 'balloon management on EditorUI#update', () => { + it( 'should not trigger if the editor is not focused', () => { + expect( badge._balloonView ).to.be.null; + + editor.ui.fire( 'update' ); + + expect( badge._balloonView ).to.be.null; + } ); + + it( 'should (re-)show the balloon but throttled', async () => { + focusEditor( editor ); + + const pinSpy = testUtils.sinon.spy( badge._balloonView, 'pin' ); + + editor.ui.fire( 'update' ); + editor.ui.fire( 'update' ); + + sinon.assert.notCalled( pinSpy ); + + await wait( 75 ); + + sinon.assert.calledOnce( pinSpy ); + sinon.assert.calledWith( pinSpy.firstCall, sinon.match.has( 'target', editor.editing.view.getDomRoot() ) ); + } ); + + it( 'should (re-)show the balloon if the focus is not in the editing root but in other editor UI', async () => { + const focusableEditorUIElement = document.createElement( 'input' ); + focusableEditorUIElement.type = 'text'; + editor.ui.focusTracker.add( focusableEditorUIElement ); + document.body.appendChild( focusableEditorUIElement ); + + focusEditor( editor, focusableEditorUIElement ); + + const pinSpy = testUtils.sinon.spy( badge._balloonView, 'pin' ); + + sinon.assert.notCalled( pinSpy ); + + editor.ui.fire( 'update' ); + editor.ui.fire( 'update' ); + + sinon.assert.calledOnce( pinSpy ); + + await wait( 75 ); + + sinon.assert.calledTwice( pinSpy ); + sinon.assert.calledWith( pinSpy, sinon.match.has( 'target', editor.editing.view.getDomRoot() ) ); + focusableEditorUIElement.remove(); + } ); + } ); + + describe( 'balloon view', () => { + let balloon, focusTrackerAddSpy; + + beforeEach( () => { + focusTrackerAddSpy = testUtils.sinon.spy( editor.ui.focusTracker, 'add' ); + + focusEditor( editor ); + + balloon = badge._balloonView; + } ); + + it( 'should be an instance of BalloonPanelView', () => { + expect( balloon ).to.be.instanceOf( BalloonPanelView ); + } ); + + it( 'should host a badge view', () => { + expect( balloon.content.first ).to.be.instanceOf( View ); + } ); + + it( 'should have no arrow', () => { + expect( balloon.withArrow ).to.be.false; + } ); + + it( 'should not have a specific CSS class if not provided', () => { + expect( balloon.class ).to.be.undefined; + } ); + + it( 'should be added to editor\'s body view collection', () => { + expect( editor.ui.view.body.has( balloon ) ).to.be.true; + } ); + + it( 'should be registered in the focus tracker to avoid focus loss on click', () => { + sinon.assert.calledWith( focusTrackerAddSpy, balloon.element ); + } ); + } ); + + describe( 'badge view', () => { + let view; + + beforeEach( () => { + focusEditor( editor ); + + view = badge._balloonView.content.first; + } ); + + it( 'should have specific CSS classes', () => { + expect( view.element.classList.contains( 'ck-badge-extended' ) ).to.be.true; + } ); + + it( 'should have a label', () => { + expect( view.element.firstChild.tagName ).to.equal( 'SPAN' ); + expect( view.element.firstChild.classList.contains( 'ck-badge-extended__label' ) ).to.be.true; + expect( view.element.firstChild.textContent ).to.equal( 'Badge extended label' ); + } ); + + it( 'should not be accessible via tab key navigation', () => { + expect( view.element.firstChild.tabIndex ).to.equal( -1 ); + } ); + } ); + } ); + + describe( 'balloon positioning depending on environment and configuration', () => { + const originalGetVisible = Rect.prototype.getVisible; + let rootRect, balloonRect; + + beforeEach( () => { + rootRect = new Rect( { top: 0, left: 0, width: 400, right: 400, bottom: 100, height: 100 } ); + balloonRect = new Rect( { top: 0, left: 0, width: 20, right: 20, bottom: 10, height: 10 } ); + } ); + + it( 'should not show the balloon if the root is not visible vertically', async () => { + const domRoot = editor.editing.view.getDomRoot(); + const parentWithOverflow = document.createElement( 'div' ); + + parentWithOverflow.style.overflow = 'scroll'; + // Is not enough height to be visible vertically. + parentWithOverflow.style.height = '99px'; + + document.body.appendChild( parentWithOverflow ); + parentWithOverflow.appendChild( domRoot ); + + focusEditor( editor ); + + expect( badge._balloonView.isVisible ).to.be.true; + expect( badge._balloonView.position ).to.equal( 'arrowless' ); + + parentWithOverflow.remove(); + } ); + + it( 'should not show the balloon if the root is not visible horizontally', async () => { + const domRoot = editor.editing.view.getDomRoot(); + const parentWithOverflow = document.createElement( 'div' ); + + parentWithOverflow.style.overflow = 'scroll'; + // Is not enough width to be visible horizontally. + parentWithOverflow.style.width = '399px'; + + document.body.appendChild( parentWithOverflow ); + parentWithOverflow.appendChild( domRoot ); + + focusEditor( editor ); + + expect( badge._balloonView.isVisible ).to.be.true; + expect( badge._balloonView.position ).to.equal( 'arrowless' ); + + parentWithOverflow.remove(); + } ); + + it( 'should position to the left side if the UI language is RTL and no side was configured', async () => { + const editor = await createEditor( element, { + language: 'ar' + } ); + + badge = new BadgeExtended( editor ); + editor.fire( 'ready' ); + + testUtils.sinon.stub( editor.ui.getEditableElement( 'main' ), 'getBoundingClientRect' ).returns( { + top: 0, + left: 0, + right: 400, + width: 400, + bottom: 100, + height: 100 + } ); + + focusEditor( editor ); + + const pinSpy = testUtils.sinon.spy( badge._balloonView, 'pin' ); + + editor.ui.fire( 'update' ); + + // Throttled #update listener. + await wait( 75 ); + + sinon.assert.calledOnce( pinSpy ); + + const pinArgs = pinSpy.firstCall.args[ 0 ]; + const positioningFunction = pinArgs.positions[ 0 ]; + + expect( pinArgs.target ).to.equal( editor.editing.view.getDomRoot() ); + expect( positioningFunction( rootRect, balloonRect ) ).to.deep.equal( { + top: 95, + left: 5, + name: 'position_border-side_left', + config: { + withArrow: false + } + } ); + + await editor.destroy(); + } ); + + it( 'should position the balloon in the lower right corner by default', async () => { + focusEditor( editor ); + + const pinSpy = testUtils.sinon.spy( badge._balloonView, 'pin' ); + + editor.ui.fire( 'update' ); + + // Throttled #update listener. + await wait( 75 ); + + sinon.assert.calledOnce( pinSpy ); + + const pinArgs = pinSpy.firstCall.args[ 0 ]; + const positioningFunction = pinArgs.positions[ 0 ]; + + expect( pinArgs.target ).to.equal( editor.editing.view.getDomRoot() ); + expect( positioningFunction( rootRect, balloonRect ) ).to.deep.equal( { + top: 95, + left: 375, + name: 'position_border-side_right', + config: { + withArrow: false + } + } ); + } ); + + it( 'should hide the balloon if the root is invisible (cropped by ancestors)', async () => { + const editor = await createEditor( element ); + + badge = new BadgeExtended( editor ); + editor.fire( 'ready' ); + + const domRoot = editor.editing.view.getDomRoot(); + + rootRect = new Rect( { top: 0, left: 0, width: 100, right: 100, bottom: 10, height: 10 } ); + + testUtils.sinon.stub( rootRect, 'getVisible' ).returns( null ); + + focusEditor( editor ); + + const pinSpy = testUtils.sinon.spy( badge._balloonView, 'pin' ); + + editor.ui.fire( 'update' ); + + // Throttled #update listener. + await wait( 75 ); + + sinon.assert.calledOnce( pinSpy ); + + const pinArgs = pinSpy.firstCall.args[ 0 ]; + const positioningFunction = pinArgs.positions[ 0 ]; + + expect( pinArgs.target ).to.equal( domRoot ); + expect( positioningFunction( rootRect, balloonRect ) ).to.equal( null ); + + await editor.destroy(); + } ); + + it( 'should hide the balloon if displayed over the bottom root border but partially cropped by an ancestor', async () => { + const editor = await createEditor( element ); + + badge = new BadgeExtended( editor ); + editor.fire( 'ready' ); + + const domRoot = editor.editing.view.getDomRoot(); + + rootRect = new Rect( { top: 0, left: 0, width: 100, right: 100, bottom: 10, height: 10 } ); + + focusEditor( editor ); + + const pinSpy = testUtils.sinon.spy( badge._balloonView, 'pin' ); + + editor.ui.fire( 'update' ); + + // Throttled #update listener. + await wait( 75 ); + + sinon.assert.calledOnce( pinSpy ); + + const pinArgs = pinSpy.firstCall.args[ 0 ]; + const positioningFunction = pinArgs.positions[ 0 ]; + + expect( pinArgs.target ).to.equal( domRoot ); + expect( positioningFunction( rootRect, balloonRect ) ).to.equal( null ); + + await editor.destroy(); + } ); + + it( 'should not display the balloon if the root is narrower than 350px', async () => { + const domRoot = editor.editing.view.getDomRoot(); + + testUtils.sinon.stub( Rect.prototype, 'getVisible' ).callsFake( function() { + if ( this._source === domRoot ) { + return new Rect( domRoot ); + } else { + return originalGetVisible.call( this ); + } + } ); + + domRoot.getBoundingClientRect.returns( { + top: 0, + left: 0, + right: 349, + width: 349, + bottom: 100, + height: 100 + } ); + + focusEditor( editor ); + + editor.ui.fire( 'update' ); + + // Throttled #update listener. + await wait( 75 ); + + const pinSpy = testUtils.sinon.spy( badge._balloonView, 'pin' ); + + expect( badge._balloonView.isVisible ).to.be.true; + expect( badge._balloonView.position ).to.equal( 'arrowless' ); + + domRoot.getBoundingClientRect.returns( { + top: 0, + left: 0, + right: 350, + width: 350, + bottom: 100, + height: 100 + } ); + + editor.ui.fire( 'update' ); + + // Throttled #update listener. + await wait( 75 ); + + expect( badge._balloonView.isVisible ).to.be.true; + expect( badge._balloonView.position ).to.equal( 'position_border-side_right' ); + + const pinArgs = pinSpy.firstCall.args[ 0 ]; + const positioningFunction = pinArgs.positions[ 0 ]; + + expect( pinArgs.target ).to.equal( editor.editing.view.getDomRoot() ); + expect( positioningFunction( rootRect, balloonRect ) ).to.deep.equal( { + top: 95, + left: 325, + name: 'position_border-side_right', + config: { + withArrow: false + } + } ); + } ); + + it( 'should not display the balloon if the root is shorter than 50px', async () => { + const domRoot = editor.editing.view.getDomRoot(); + + testUtils.sinon.stub( Rect.prototype, 'getVisible' ).callsFake( function() { + if ( this._source === domRoot ) { + return new Rect( domRoot ); + } else { + return originalGetVisible.call( this ); + } + } ); + + domRoot.getBoundingClientRect.returns( { + top: 0, + left: 0, + right: 1000, + width: 1000, + bottom: 49, + height: 49 + } ); + + focusEditor( editor ); + + editor.ui.fire( 'update' ); + + // Throttled #update listener. + await wait( 75 ); + + const pinSpy = testUtils.sinon.spy( badge._balloonView, 'pin' ); + + expect( badge._balloonView.isVisible ).to.be.true; + expect( badge._balloonView.position ).to.equal( 'arrowless' ); + + domRoot.getBoundingClientRect.returns( { + top: 0, + left: 0, + right: 1000, + width: 1000, + bottom: 50, + height: 50 + } ); + + editor.ui.fire( 'update' ); + + // Throttled #update listener. + await wait( 75 ); + + expect( badge._balloonView.isVisible ).to.be.true; + expect( badge._balloonView.position ).to.equal( 'position_border-side_right' ); + + const pinArgs = pinSpy.firstCall.args[ 0 ]; + const positioningFunction = pinArgs.positions[ 0 ]; + + expect( pinArgs.target ).to.equal( editor.editing.view.getDomRoot() ); + expect( positioningFunction( rootRect, balloonRect ) ).to.deep.equal( { + top: 45, + left: 975, + name: 'position_border-side_right', + config: { + withArrow: false + } + } ); + } ); + } ); + + async function createEditor( element, config = { plugins: [ SourceEditing ] } ) { + return ClassicTestEditor.create( element, config ); + } + + function wait( time ) { + return new Promise( res => { + window.setTimeout( res, time ); + } ); + } + + function focusEditor( editor, focusableUIElement ) { + if ( !focusableUIElement ) { + focusableUIElement = editor.editing.view.getDomRoot(); + editor.editing.view.focus(); + } else { + focusableUIElement.focus(); + } + + editor.ui.focusTracker.focusedElement = focusableUIElement; + editor.ui.focusTracker.isFocused = true; + } + + function blurEditor( editor ) { + editor.ui.focusTracker.focusedElement = null; + editor.ui.focusTracker.isFocused = null; + } +} ); From bb96dc27ad4a74cf5fabe833af5472df56efda02 Mon Sep 17 00:00:00 2001 From: Marta Motyczynska Date: Wed, 3 Jul 2024 13:53:42 +0200 Subject: [PATCH 063/256] Reorder imports. --- packages/ckeditor5-ui/tests/editorui/poweredby.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/ckeditor5-ui/tests/editorui/poweredby.js b/packages/ckeditor5-ui/tests/editorui/poweredby.js index 62c4aa05184..e18426f325d 100644 --- a/packages/ckeditor5-ui/tests/editorui/poweredby.js +++ b/packages/ckeditor5-ui/tests/editorui/poweredby.js @@ -7,18 +7,18 @@ import { Editor } from '@ckeditor/ckeditor5-core'; import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor.js'; -import EditorUI from '../../src/editorui/editorui.js'; -import { BalloonPanelView } from '../../src/index.js'; -import View from '../../src/view.js'; - import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor.js'; -import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils.js'; -import { Rect, global } from '@ckeditor/ckeditor5-utils'; import SourceEditing from '@ckeditor/ckeditor5-source-editing/src/sourceediting.js'; import Heading from '@ckeditor/ckeditor5-heading/src/heading.js'; +import { Rect, global } from '@ckeditor/ckeditor5-utils'; +import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils.js'; import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model.js'; import generateKey from '@ckeditor/ckeditor5-core/tests/_utils/generatelicensekey.js'; +import EditorUI from '../../src/editorui/editorui.js'; +import { BalloonPanelView } from '../../src/index.js'; +import View from '../../src/view.js'; + describe( 'PoweredBy', () => { let editor, element; From 67143b754cf13e285e8c8ffb545db2f3d2a68a4a Mon Sep 17 00:00:00 2001 From: Marta Motyczynska Date: Wed, 3 Jul 2024 16:02:57 +0200 Subject: [PATCH 064/256] Update css for evaluation badge. --- packages/ckeditor5-ui/theme/globals/_evaluationbadge.css | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/ckeditor5-ui/theme/globals/_evaluationbadge.css b/packages/ckeditor5-ui/theme/globals/_evaluationbadge.css index 3ab6e3fcb86..f0f18bd592a 100644 --- a/packages/ckeditor5-ui/theme/globals/_evaluationbadge.css +++ b/packages/ckeditor5-ui/theme/globals/_evaluationbadge.css @@ -4,7 +4,8 @@ */ :root { - --ck-evaluation-badge-line-height: 10px; + --ck-evaluation-badge-font-size: 7.5px; + --ck-evaluation-badge-line-height: 7.5px; --ck-evaluation-badge-padding-vertical: 2px; --ck-evaluation-badge-padding-horizontal: 4px; --ck-evaluation-badge-text-color: hsl(0, 0%, 31%); @@ -23,10 +24,10 @@ & .ck.ck-evaluation-badge { line-height: var(--ck-evaluation-badge-line-height); - padding: var(--ck-powered-by-padding-vertical) var(--ck-powered-by-padding-horizontal); + padding: var(--ck-evaluation-badge-padding-vertical) var(--ck-evaluation-badge-padding-horizontal); & .ck-evaluation-badge__label { - font-size: 7.5px; + font-size: var(--ck-evaluation-badge-font-size); letter-spacing: -.2px; padding-left: 2px; text-transform: uppercase; From 0f9ebfe1b7f685ac0762ce7a54633f8aed3a15dd Mon Sep 17 00:00:00 2001 From: Marta Motyczynska Date: Mon, 8 Jul 2024 16:13:18 +0200 Subject: [PATCH 065/256] Fix typo. --- packages/ckeditor5-core/src/editor/editor.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ckeditor5-core/src/editor/editor.ts b/packages/ckeditor5-core/src/editor/editor.ts index 5003ac0ce25..4e5bfd8c81d 100644 --- a/packages/ckeditor5-core/src/editor/editor.ts +++ b/packages/ckeditor5-core/src/editor/editor.ts @@ -492,10 +492,10 @@ export default abstract class Editor extends /* #__PURE__ */ ObservableMixin() { * Your license key cannot be validated because of a network issue. * Please make sure that your setup does not block the request. * - * @error license-key-validaton-endpoint-not-reachable + * @error license-key-validation-endpoint-not-reachable * @param {String} url The URL that was attempted to reach. */ - logError( 'license-key-validaton-endpoint-not-reachable', { url: licensePayload.usageEndpoint } ); + logError( 'license-key-validation-endpoint-not-reachable', { url: licensePayload.usageEndpoint } ); } ); }, { priority: 'high' } ); } From 22ef89af279cf417480a7104f55ed4cc2f05aa9b Mon Sep 17 00:00:00 2001 From: Marta Motyczynska Date: Mon, 8 Jul 2024 16:13:18 +0200 Subject: [PATCH 066/256] Fix typo. --- packages/ckeditor5-core/src/editor/editor.ts | 4 ++-- packages/ckeditor5-core/tests/editor/licensecheck.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ckeditor5-core/src/editor/editor.ts b/packages/ckeditor5-core/src/editor/editor.ts index 5003ac0ce25..4e5bfd8c81d 100644 --- a/packages/ckeditor5-core/src/editor/editor.ts +++ b/packages/ckeditor5-core/src/editor/editor.ts @@ -492,10 +492,10 @@ export default abstract class Editor extends /* #__PURE__ */ ObservableMixin() { * Your license key cannot be validated because of a network issue. * Please make sure that your setup does not block the request. * - * @error license-key-validaton-endpoint-not-reachable + * @error license-key-validation-endpoint-not-reachable * @param {String} url The URL that was attempted to reach. */ - logError( 'license-key-validaton-endpoint-not-reachable', { url: licensePayload.usageEndpoint } ); + logError( 'license-key-validation-endpoint-not-reachable', { url: licensePayload.usageEndpoint } ); } ); }, { priority: 'high' } ); } diff --git a/packages/ckeditor5-core/tests/editor/licensecheck.js b/packages/ckeditor5-core/tests/editor/licensecheck.js index 571e5298c93..27f64dd2c62 100644 --- a/packages/ckeditor5-core/tests/editor/licensecheck.js +++ b/packages/ckeditor5-core/tests/editor/licensecheck.js @@ -531,7 +531,7 @@ describe( 'Editor - license check', () => { sinon.assert.calledOnce( fetchStub ); sinon.assert.calledWithMatch( - errorStub, 'license-key-validaton-endpoint-not-reachable', { 'url': 'https://ckeditor.com' } ); + errorStub, 'license-key-validation-endpoint-not-reachable', { 'url': 'https://ckeditor.com' } ); expect( editor.isReadOnly ).to.be.false; } ); From d0f80a68bb7c3b0c5d10823ca796a19ea6db1638 Mon Sep 17 00:00:00 2001 From: Marta Motyczynska Date: Tue, 9 Jul 2024 16:06:09 +0200 Subject: [PATCH 067/256] Fix tests for evaluation badge. --- .../tests/editorui/evaluationbadge.js | 40 ++++++++++++------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/packages/ckeditor5-ui/tests/editorui/evaluationbadge.js b/packages/ckeditor5-ui/tests/editorui/evaluationbadge.js index 05f433789f6..50243f060d8 100644 --- a/packages/ckeditor5-ui/tests/editorui/evaluationbadge.js +++ b/packages/ckeditor5-ui/tests/editorui/evaluationbadge.js @@ -20,17 +20,17 @@ import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model.js'; import generateKey from '@ckeditor/ckeditor5-core/tests/_utils/generatelicensekey.js'; describe( 'EvaluationBadge', () => { - let editor, element, trialLicenseKey; + let editor, element, developmentLicenseKey; testUtils.createSinonSandbox(); beforeEach( async () => { - trialLicenseKey = generateKey( { licenseType: 'trial' } ).licenseKey; + developmentLicenseKey = generateKey( { licenseType: 'development' } ).licenseKey; element = document.createElement( 'div' ); document.body.appendChild( element ); editor = await createEditor( element, { plugins: [ SourceEditing ], - licenseKey: trialLicenseKey + licenseKey: developmentLicenseKey } ); testUtils.sinon.stub( editor.editing.view.getDomRoot(), 'getBoundingClientRect' ).returns( { @@ -64,7 +64,7 @@ describe( 'EvaluationBadge', () => { describe( 'balloon creation', () => { it( 'should not throw if there is no view in EditorUI', () => { expect( () => { - const editor = new Editor( { licenseKey: trialLicenseKey } ); + const editor = new Editor( { licenseKey: developmentLicenseKey } ); editor.model.document.createRoot(); editor.ui = new EditorUI( editor ); @@ -89,8 +89,17 @@ describe( 'EvaluationBadge', () => { } ); it( 'should create the balloon when license type is `trial`', async () => { + const { licenseKey, todayTimestamp } = generateKey( { + licenseType: 'trial', + isExpired: false, + daysAfterExpiration: -1 + } ); + + const today = todayTimestamp; + const dateNow = sinon.stub( Date, 'now' ).returns( today ); + const editor = await createEditor( element, { - licenseKey: trialLicenseKey + licenseKey } ); expect( editor.ui.evaluationBadge._balloonView ).to.be.null; @@ -106,12 +115,13 @@ describe( 'EvaluationBadge', () => { ); await editor.destroy(); + + dateNow.restore(); } ); it( 'should create the balloon when license type is `development`', async () => { - const devLicenseKey = generateKey( { licenseType: 'development' } ).licenseKey; const editor = await createEditor( element, { - licenseKey: devLicenseKey + licenseKey: developmentLicenseKey } ); expect( editor.ui.evaluationBadge._balloonView ).to.be.null; @@ -130,7 +140,7 @@ describe( 'EvaluationBadge', () => { } ); it( 'should not depend on white-label', async () => { - const { licenseKey } = generateKey( { whiteLabel: true, licenseType: 'trial' } ); + const { licenseKey } = generateKey( { whiteLabel: true, licenseType: 'development' } ); const editor = await createEditor( element, { licenseKey } ); @@ -478,7 +488,7 @@ describe( 'EvaluationBadge', () => { it( 'should position the badge to the left right if the UI language is RTL (and powered-by is on the left)', async () => { const editor = await createEditor( element, { language: 'ar', - licenseKey: trialLicenseKey + licenseKey: developmentLicenseKey } ); testUtils.sinon.stub( editor.ui.getEditableElement( 'main' ), 'getBoundingClientRect' ).returns( { @@ -550,7 +560,7 @@ describe( 'EvaluationBadge', () => { side: 'left' } }, - licenseKey: trialLicenseKey + licenseKey: developmentLicenseKey } ); testUtils.sinon.stub( editor.ui.getEditableElement( 'main' ), 'getBoundingClientRect' ).returns( { @@ -596,7 +606,7 @@ describe( 'EvaluationBadge', () => { position: 'border' } }, - licenseKey: trialLicenseKey + licenseKey: developmentLicenseKey } ); testUtils.sinon.stub( editor.ui.getEditableElement( 'main' ), 'getBoundingClientRect' ).returns( { @@ -642,7 +652,7 @@ describe( 'EvaluationBadge', () => { position: 'inside' } }, - licenseKey: trialLicenseKey + licenseKey: developmentLicenseKey } ); testUtils.sinon.stub( editor.ui.getEditableElement( 'main' ), 'getBoundingClientRect' ).returns( { @@ -688,7 +698,7 @@ describe( 'EvaluationBadge', () => { position: 'border' } }, - licenseKey: trialLicenseKey + licenseKey: developmentLicenseKey } ); const domRoot = editor.editing.view.getDomRoot(); @@ -722,7 +732,7 @@ describe( 'EvaluationBadge', () => { position: 'inside' } }, - licenseKey: trialLicenseKey + licenseKey: developmentLicenseKey } ); rootRect = new Rect( { top: 0, left: 0, width: 400, right: 400, bottom: 200, height: 200 } ); @@ -907,7 +917,7 @@ describe( 'EvaluationBadge', () => { position: 'inside' } }, - licenseKey: trialLicenseKey + licenseKey: developmentLicenseKey } ); setData( editor.model, 'foo[]bar' ); From 77ab3810efe7a33ecf8cc7ec1e8bfb0169a14e18 Mon Sep 17 00:00:00 2001 From: Illia Sheremetov Date: Tue, 16 Jul 2024 13:57:43 +0200 Subject: [PATCH 068/256] Change variable name. --- packages/ckeditor5-core/src/editor/editor.ts | 2 +- packages/ckeditor5-core/tests/editor/licensecheck.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ckeditor5-core/src/editor/editor.ts b/packages/ckeditor5-core/src/editor/editor.ts index 4e5bfd8c81d..5d7127e86b7 100644 --- a/packages/ckeditor5-core/src/editor/editor.ts +++ b/packages/ckeditor5-core/src/editor/editor.ts @@ -373,7 +373,7 @@ export default abstract class Editor extends /* #__PURE__ */ ObservableMixin() { function verifyLicenseKey( editor: Editor ) { const licenseKey = editor.config.get( 'licenseKey' )!; - const distributionChannel = ( window as any )[ 'CKE_DISTRIBUTION ' ] || 'sh'; + const distributionChannel = ( window as any )[ Symbol.for( 'cke distribution' ) ] || 'sh'; if ( licenseKey == 'GPL' ) { if ( distributionChannel == 'cloud' ) { diff --git a/packages/ckeditor5-core/tests/editor/licensecheck.js b/packages/ckeditor5-core/tests/editor/licensecheck.js index 27f64dd2c62..91fb9314a81 100644 --- a/packages/ckeditor5-core/tests/editor/licensecheck.js +++ b/packages/ckeditor5-core/tests/editor/licensecheck.js @@ -111,7 +111,7 @@ describe( 'Editor - license check', () => { describe( 'distribution channel check', () => { afterEach( () => { - delete window[ 'CKE_DISTRIBUTION ' ]; + delete window[ Symbol.for( 'cke distribution' ) ]; } ); it( 'should not block if distribution channel match', () => { @@ -210,7 +210,7 @@ describe( 'Editor - license check', () => { } ); function setChannel( channel ) { - window[ 'CKE_DISTRIBUTION ' ] = channel; + window[ Symbol.for( 'cke distribution' ) ] = channel; } } ); From a4890794a911f4a5fd0aaa345ea68450d99fe133 Mon Sep 17 00:00:00 2001 From: Marta Motyczynska Date: Wed, 17 Jul 2024 16:18:31 +0200 Subject: [PATCH 069/256] Add proper headers for sending request. --- packages/ckeditor5-core/src/editor/editor.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/ckeditor5-core/src/editor/editor.ts b/packages/ckeditor5-core/src/editor/editor.ts index 4e5bfd8c81d..71761efcf7a 100644 --- a/packages/ckeditor5-core/src/editor/editor.ts +++ b/packages/ckeditor5-core/src/editor/editor.ts @@ -961,8 +961,10 @@ export default abstract class Editor extends /* #__PURE__ */ ObservableMixin() { } private async _sendUsageRequest( endpoint: string, request: unknown ) { + const headers = new Headers( { 'Content-Type': 'application/json' } ); const response = await fetch( new URL( endpoint ), { method: 'POST', + headers, body: JSON.stringify( request ) } ); From 31b86cf6d28269b1bed8f1fd9355f4f31547de92 Mon Sep 17 00:00:00 2001 From: Marta Motyczynska Date: Thu, 18 Jul 2024 09:28:37 +0200 Subject: [PATCH 070/256] Take care of empty licensedHosts in license key. --- packages/ckeditor5-core/src/editor/editor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ckeditor5-core/src/editor/editor.ts b/packages/ckeditor5-core/src/editor/editor.ts index f867b5941f0..813d67d435f 100644 --- a/packages/ckeditor5-core/src/editor/editor.ts +++ b/packages/ckeditor5-core/src/editor/editor.ts @@ -427,7 +427,7 @@ export default abstract class Editor extends /* #__PURE__ */ ObservableMixin() { const licensedHosts: Array | undefined = licensePayload.licensedHosts; - if ( licensedHosts ) { + if ( licensedHosts && licensedHosts.length > 0 ) { const hostname = window.location.hostname; const willcards = licensedHosts .filter( val => val.startsWith( '*' ) ) From a767ed9896778dfeff7169700ce08e108ed6011e Mon Sep 17 00:00:00 2001 From: godai78 Date: Fri, 19 Jul 2024 05:53:31 +0200 Subject: [PATCH 071/256] Docs: new structure. [short flow] --- .../legacy-getting-started/quick-start.md | 2 +- .../licensing/license-and-legal.md | 29 +++++ .../licensing/license-key-and-activation.md | 102 ++++++++++++++++++ docs/getting-started/licensing/plans.md | 11 ++ docs/getting-started/licensing/ubb.md | 10 ++ docs/getting-started/quick-start.md | 2 +- docs/getting-started/setup/configuration.md | 2 +- .../setup/managing-ckeditor-logo.md | 2 +- docs/umberto.json | 28 ++--- 9 files changed, 170 insertions(+), 18 deletions(-) create mode 100644 docs/getting-started/licensing/license-and-legal.md create mode 100644 docs/getting-started/licensing/license-key-and-activation.md create mode 100644 docs/getting-started/licensing/plans.md create mode 100644 docs/getting-started/licensing/ubb.md diff --git a/docs/getting-started/legacy-getting-started/quick-start.md b/docs/getting-started/legacy-getting-started/quick-start.md index 58e7a452101..dcdca820ec7 100644 --- a/docs/getting-started/legacy-getting-started/quick-start.md +++ b/docs/getting-started/legacy-getting-started/quick-start.md @@ -95,7 +95,7 @@ The fastest way to run an advanced editor using the {@link features/index rich e In the superbuild, all editor classes are stored under the `CKEDITOR` object. Apart from that exception, the editor initialization is no different than the one described in the {@link getting-started/legacy-getting-started/predefined-builds#available-builds available builds documentation}. -Because the superbuild contains a lot of plugins, you may need to remove the plugins you do not need with the `removePlugins` configuration option and adjust the toolbar configuration. There are also some plugins, like the {@link features/productivity-pack Productivity Pack}, that require a license to run. You can learn more about obtaining and activating license keys in the {@link getting-started/setup/license-key-and-activation License key and activation} guide. Observe the configuration below to see this implemented. +Because the superbuild contains a lot of plugins, you may need to remove the plugins you do not need with the `removePlugins` configuration option and adjust the toolbar configuration. There are also some plugins, like the {@link features/productivity-pack Productivity Pack}, that require a license to run. You can learn more about obtaining and activating license keys in the {@link getting-started/licensing/license-key-and-activation License key and activation} guide. Observe the configuration below to see this implemented. ### Sample implementation diff --git a/docs/getting-started/licensing/license-and-legal.md b/docs/getting-started/licensing/license-and-legal.md new file mode 100644 index 00000000000..f99dd3d93fa --- /dev/null +++ b/docs/getting-started/licensing/license-and-legal.md @@ -0,0 +1,29 @@ +--- +# Scope: +# * Clarify copyright and license conditions. + +category: licensing +meta-title: Editor license | CKEditor 5 Documentation +menu-title: Editor license +order: 10 +--- + +# License and legal + +The following legal notices apply to CKEditor 5 and all software from CKEditor 5 Ecosystem included with it. + +Copyright (c) 2003–2024, CKSource Holding sp. z o.o. All rights reserved. + +Licensed under the terms of [GNU General Public License Version 2 or later](http://www.gnu.org/licenses/old-licenses/gpl-2.0.html). + +## Free for Open Source + +If you are running an Open Source project under an OSS license incompatible with GPL, please [contact us](https://ckeditor.com/contact/). We will be happy to [support your project with a free CKEditor 5 license](https://ckeditor.com/wysiwyg-editor-open-source/). + +## Sources of intellectual property included in CKEditor + +Where not otherwise indicated, all CKEditor 5 content is authored by CKSource engineers and consists of CKSource-owned intellectual property. In some specific instances, CKEditor will incorporate work done by developers outside of CKSource with their express permission. + +## Trademarks + +CKEditor is a trademark of [CKSource Holding sp. z o.o.](http://cksource.com/) All other brand and product names are trademarks, registered trademarks, or service marks of their respective holders. diff --git a/docs/getting-started/licensing/license-key-and-activation.md b/docs/getting-started/licensing/license-key-and-activation.md new file mode 100644 index 00000000000..accfca2505a --- /dev/null +++ b/docs/getting-started/licensing/license-key-and-activation.md @@ -0,0 +1,102 @@ +--- +category: licensing +order: 15 +meta-title: License key and activation | CKEditor 5 Documentation +menu-title: License key and activation +--- + +# License key and activation + +This article explains how to activate a commercial license of CKEditor 5 and the following CKEditor premium features: + +* Asynchronous collaboration features, including: + * {@link features/track-changes Track changes} + * {@link features/comments Comments} + * {@link features/revision-history Revision history} +* {@link features/pagination Pagination} +* {@link features/ai-assistant-overview AI Assistant} +* {@link features/multi-level-lists Multi-level list} +* The Productivity Pack that includes: + * {@link features/case-change Case change} + * {@link features/document-outline Document outline} + * {@link features/format-painter Format painter} + * {@link features/paste-from-office-enhanced Paste from Office enhanced} + * {@link features/slash-commands Slash commands} + * {@link features/table-of-contents Table of contents} + * {@link features/template Templates} + +Other premium features such as {@link features/real-time-collaboration real-time collaboration}, {@link features/export-word export to Word}, {@link features/export-pdf export to PDF}, or {@link features/import-word import from Word} are authenticated on the server side. Please refer to respective feature guides for installation details. + + + CKEditor 5 (without premium features listed above) can be used without activation as {@link getting-started/licensing/license-and-legal open source software under the GPL license}. It will then {@link getting-started/setup/managing-ckeditor-logo display a small "Powered by CKEditor" logo} in the editor area. + + +## Obtaining a license + +To activate CKEditor 5 and the premium features listed above, you will need either an active commercial license or a trial license. + +### Purchasing a commercial license + +If you wish to purchase a commercial CKEditor 5 license or a license to one of the premium features, [contact us](https://ckeditor.com/contact/?sales=true#contact-form) to receive an offer tailored to your needs. + +### Subscribing to the CKEditor Premium Features free trial + +If you wish to test our offer, you can create an account by [signing up for CKEditor Premium Features 30-day free trial](https://orders.ckeditor.com/trial/premium-features). After signing up, you will receive access to the customer dashboard (CKEditor Ecosystem dashboard). + +The trial is commitment-free, and there is no need to provide credit card details to start it. The Premium Features free trial allows you to test all paid CKEditor Ecosystem products at no cost. + +If you are using the trial, refer to the [CKEditor 5 Premium Features free trial documentation](https://ckeditor.com/docs/trial/latest/guides/overview.html) to learn how to access the relevant license key and activate the premium features. + +## Obtaining a license key + +Follow this guide to get the license key necessary to activate your purchased premium features or to white-label CKEditor 5 (remove the "Powered by CKEditor" logo). + +### Log in to the CKEditor Ecosystem dashboard + +Log in to the [CKEditor Ecosystem dashboard](https://dashboard.ckeditor.com). If this is the first time you do it, you will receive a confirmation email and will be asked to create a password for your account. Keep it safe. + +### Access the account dashboard + +After logging in, click "CKEditor" under the "Your products" header on the left. You will see the overview of the subscription parameters together with the management area below. + +{@img assets/img/ckeditor-dashboard.png 920 Your CKEditor subscriptions in the customer dashboard.} + +### Copy the license key + +After clicking "Manage," you can access the license key needed to run the editor and the premium features. Note that the same license key will be valid for both the Productivity Pack and other standalone features, as well as CKEditor 5 itself. + +{@img assets/img/ckeditor-key.png 822 Premium features license key in the management console.} + +There are two license keys available: +1. The old key for versions older than 38.0.0. +2. The new key for versions 38.0.0 and later. + +The new key available is the new format license key that is **only** valid for versions 38.0.0 or later. The old key will work with all CKEditor 5 versions up to the version to be released in May 2024 (when we consider removing support for these keys) as long as the key is not expired. + + + Please note that the Premium Features Free Trial dashboard only provides one license key. This key is meant to work with the most recent version of CKEditor 5. + + +## Activating the product + +You need to add the license key to your CKEditor 5 configuration. It is enough to add the license key once for the standalone features listed in this guide, no matter which and how many premium features you intend to use. + +```js +ClassicEditor + .create( document.querySelector( '#editor' ), { + // Load the plugin. + plugins: [ /* ... */ ], + + // Provide the licence key. + licenseKey: '', + + // Display the feature UI element in the toolbar. + toolbar: [ /* ... */ ], + } ) + .then( /* ... */ ) + .catch( /* ... */ ); +``` + +To use premium features, you need to add the relevant plugins to your CKEditor 5. You can use the [CKEditor 5 Builder](https://ckeditor.com/ckeditor-5/builder?redirect=docs) to generate a CKEditor 5 preset with the plugins enabled. + +Alternatively, refer to the installation sections in the plugin documentation to do it on your own. You can read more about {@link getting-started/setup/configuration installing plugins} and {@link getting-started/setup/toolbar toolbar configuration} in dedicated guides. diff --git a/docs/getting-started/licensing/plans.md b/docs/getting-started/licensing/plans.md new file mode 100644 index 00000000000..123f33ecfc0 --- /dev/null +++ b/docs/getting-started/licensing/plans.md @@ -0,0 +1,11 @@ +--- +category: licensing +menu-title: Available plans +meta-title: Configuring editor features | CKEditor 5 documentation +meta-description: Learn how to configure CKEditor 5. +order: 20 +modified_at: 2024-06-25 +--- + +# Configuring CKEditor 5 features + diff --git a/docs/getting-started/licensing/ubb.md b/docs/getting-started/licensing/ubb.md new file mode 100644 index 00000000000..de270ebd0ad --- /dev/null +++ b/docs/getting-started/licensing/ubb.md @@ -0,0 +1,10 @@ +--- +category: licensing +menu-title: Usage-based billing +meta-title: Configuring editor features | CKEditor 5 documentation +meta-description: Learn how to configure CKEditor 5. +order: 30 +modified_at: 2024-06-25 +--- + +# Configuring CKEditor 5 features diff --git a/docs/getting-started/quick-start.md b/docs/getting-started/quick-start.md index 8d4fba4d865..ccca04f8ec1 100644 --- a/docs/getting-started/quick-start.md +++ b/docs/getting-started/quick-start.md @@ -422,7 +422,7 @@ All three installation methods – npm, CDN, ZIP – work similarly. So, To activate CKEditor 5 premium features, you will need a commercial license. The easiest way to get one is to sign up for the [CKEditor Premium Features 30-day free trial](https://orders.ckeditor.com/trial/premium-features) to test the premium features. -You can also [contact us](https://ckeditor.com/contact/?sales=true#contact-form) to receive an offer tailored to your needs. To obtain an activation key, please follow the {@link getting-started/setup/license-key-and-activation License key and activation} guide. +You can also [contact us](https://ckeditor.com/contact/?sales=true#contact-form) to receive an offer tailored to your needs. To obtain an activation key, please follow the {@link getting-started/licensing/license-key-and-activation License key and activation} guide. ## Next steps diff --git a/docs/getting-started/setup/configuration.md b/docs/getting-started/setup/configuration.md index deddfd8c079..613c01c477f 100644 --- a/docs/getting-started/setup/configuration.md +++ b/docs/getting-started/setup/configuration.md @@ -38,7 +38,7 @@ Note that some features may require more than one plugin to run, as shown above. ### Adding premium features -CKEditor 5 premium features are imported in the same way. However, they have their own package, named `ckeditor5-premium-features`, to import from. These also {@link getting-started/setup/license-key-and-activation require a license}. Please see an example below, adding the PDF export feature and configuring it. +CKEditor 5 premium features are imported in the same way. However, they have their own package, named `ckeditor5-premium-features`, to import from. These also {@link getting-started/licensing/license-key-and-activation require a license}. Please see an example below, adding the PDF export feature and configuring it. ```js import { ClassicEditor } from 'ckeditor5'; diff --git a/docs/getting-started/setup/managing-ckeditor-logo.md b/docs/getting-started/setup/managing-ckeditor-logo.md index 390b7e41ac3..138b7df3278 100644 --- a/docs/getting-started/setup/managing-ckeditor-logo.md +++ b/docs/getting-started/setup/managing-ckeditor-logo.md @@ -23,7 +23,7 @@ However, even as a paid customer, you can [keep the logo](#how-to-keep-the-power To remove the logo, you need to obtain a commercial license and then configure the {@link module:core/editor/editorconfig~EditorConfig#licenseKey `config.licenseKey`} setting. -Refer to the {@link getting-started/setup/license-key-and-activation License key and activation} guide for details on where to find the license key and how to use it in your configuration. +Refer to the {@link getting-started/licensing/license-key-and-activation License key and activation} guide for details on where to find the license key and how to use it in your configuration. ## How to keep the "Powered by CKEditor" logo? diff --git a/docs/umberto.json b/docs/umberto.json index 11610bd84fb..8d7674413dc 100644 --- a/docs/umberto.json +++ b/docs/umberto.json @@ -282,8 +282,8 @@ "installation/getting-started/configuration.html#adding-simple-standalone-features": "installation/legacy-getting-started/extending-features.html", "support/getting-support.html": "support/index.html", "support/versioning-policy.html": "updating/versioning-policy.html", - "builds/guides/license-and-legal.html": "support/license-and-legal.html", - "builds/guides/support/license-and-legal.html": "support/license-and-legal.html", + "builds/guides/license-and-legal.html": "getting-started/licensing/license-and-legal.html", + "builds/guides/getting-started/licensing/license-and-legal.html": "getting-started/licensing/license-and-legal.html", "support/managing-ckeditor-logo.html": "getting-started/setup/managing-ckeditor-logo.html", "support/faq.html": "examples/how-tos.html", "updating/changelog.html": "updating/guides/changelog.html", @@ -356,10 +356,12 @@ "tutorials/widgets/implementing-an-inline-widget.html": "framework/tutorials/widgets/implementing-an-inline-widget.html", "tutorials/widgets/using-react-in-a-widget.html": "framework/tutorials/widgets/using-react-in-a-widget.html", "tutorials/widgets/data-from-external-source.html": "framework/tutorials/widgets/data-from-external-source.html", - "support/licensing/license-and-legal.html": "support/license-and-legal.html", - "support/licensing/license-key-and-activation.html": "getting-started/setup/license-key-and-activation.html", + "support/licensing/license-and-legal.html": "getting-started/licensing/license-and-legal.html", + "support/licensing/license-key-and-activation.html": "getting-started/licensing/license-key-and-activation.html", + "getting-started/setup/license-key-and-activation.html": "getting-started/licensing/license-key-and-activation.html", "support/licensing/managing-ckeditor-logo.html": "getting-started/setup/managing-ckeditor-logo.html", "support/reporting-issues.html": "support/index.html#reporting-issues", + "support/license-and-legal": "getting-started/licensing/license-and-legal", "examples/experiments/mermaid.html": "features/mermaid.html", "features/toolbar/toolbar.html": "getting-started/setup/toolbar.html", "features/toolbar/blocktoolbar.html": "getting-started/setup/toolbar.html#block-toolbar", @@ -415,7 +417,7 @@ } }, "og": { - "description": "Learn how to install, integrate, configure, and develop CKEditor 5. Get to know the CKEditor 5 Framework. Browse through the API documentation and online samples." + "description": "Learn how to install, integrate and configure CKEditor 5 Builds and how to work with CKEditor 5 Framework, customize it, create your own plugins and custom editors, change the UI or even bring your own UI to the editor. API reference and examples included." }, "groups": [ { @@ -444,6 +446,12 @@ "slug": "setup", "order": 40 }, + { + "name": "Licensing", + "id": "licensing", + "slug": "licensing", + "order": 50 + }, { "name": "Legacy installation methods", "id": "legacy", @@ -783,15 +791,7 @@ "name": "Support", "id": "support", "navigationIncludeIndex": true, - "slug": "support", - "categories": [ - { - "name": "Licensing and activation", - "id": "licensing", - "slug": "licensing", - "order": 20 - } - ] + "slug": "support" } ] } From a86325d4798e9f50d90a94c55732a20b2061bb19 Mon Sep 17 00:00:00 2001 From: godai78 Date: Fri, 19 Jul 2024 06:38:52 +0200 Subject: [PATCH 072/256] Docs: license. [short flow] --- .../licensing/license-and-legal.md | 42 ++++++++++++++++++- .../licensing/license-key-and-activation.md | 2 + docs/getting-started/licensing/plans.md | 5 ++- docs/umberto.json | 2 +- 4 files changed, 48 insertions(+), 3 deletions(-) diff --git a/docs/getting-started/licensing/license-and-legal.md b/docs/getting-started/licensing/license-and-legal.md index f99dd3d93fa..ddafbc138bd 100644 --- a/docs/getting-started/licensing/license-and-legal.md +++ b/docs/getting-started/licensing/license-and-legal.md @@ -8,7 +8,7 @@ menu-title: Editor license order: 10 --- -# License and legal +# Editor license and legal terms The following legal notices apply to CKEditor 5 and all software from CKEditor 5 Ecosystem included with it. @@ -20,6 +20,46 @@ Licensed under the terms of [GNU General Public License Version 2 or later](http If you are running an Open Source project under an OSS license incompatible with GPL, please [contact us](https://ckeditor.com/contact/). We will be happy to [support your project with a free CKEditor 5 license](https://ckeditor.com/wysiwyg-editor-open-source/). + +## Available commercial licenses + +### Trial license key + +This key grants access to **all features**. Valid for **14 days**. It does not consume editor loads, but editor is limited functionally (for example session time, number of changes). It is **perfect for evaluating the platform** and all its features. It can be used only for evaluation purposes. + +* **Features**: Grants access to all features and add-ons. +* **Duration**: Valid for 14 days (until 12th May 2024). +* **Functionality**: The editor is limited functionally, such as session time and the number of changes allowed. +* **Intended Use**: Ideal for evaluating the platform and all its features. +* **Usage Limitation**: Can only be used for evaluation purposes and not for production. +* **Editor Loads**: Does not consume editor loads. + +You can sign up for the [CKEditor Premium Features 14-day free trial](https://orders.ckeditor.com/trial/premium-features) to test the editor. + +### Development license key + +This key grants access to your subscription features. It does not consume editor loads, but editor is limited functionally (for example session time, number of changes, development domains). It is **perfect for development environments** (local work, CI, E2E tests). It must not be used for production environments. + +* **Features**: Grants access to subscription features. +* **Functionality**: Similar to the trial license, the editor is limited functionally, including session time and the number of changes allowed. Additionally, there might be limitations on development domains. +* **Intended Use**: Designed for development environments such as local work, continuous integration (CI), and end-to-end (E2E) tests. +* **Usage Limitation**: Must not be used for production environments. +* **Editor Loads**: Does not consume editor loads. + +[Contact us](https://ckeditor.com/contact/?sales=true#contact-form) for more details + +### Production license key + +This key grants access to your subscription features without imposing any limitations. It **consumes editor loads** (after the 14 days trial period ends). + +* **Features**: Grants access to subscription features. +* **Functionality**: The editor functions without any restrictions. +* **Intended Use**: Meant for production environments where the software is actively used by end-users. +* **Usage Limitation**: None specified. +* **Editor Loads**: Consumes editor loads, especially after the 14-day trial period ends. + +There are {@link getting-started/licensing/plans several commercial plans available} to choose from. [Contact us](https://ckeditor.com/contact/?sales=true#contact-form) for more details + ## Sources of intellectual property included in CKEditor Where not otherwise indicated, all CKEditor 5 content is authored by CKSource engineers and consists of CKSource-owned intellectual property. In some specific instances, CKEditor will incorporate work done by developers outside of CKSource with their express permission. diff --git a/docs/getting-started/licensing/license-key-and-activation.md b/docs/getting-started/licensing/license-key-and-activation.md index accfca2505a..2e5b3b855de 100644 --- a/docs/getting-started/licensing/license-key-and-activation.md +++ b/docs/getting-started/licensing/license-key-and-activation.md @@ -29,6 +29,8 @@ Other premium features such as {@link features/real-time-collaboration real-time CKEditor 5 (without premium features listed above) can be used without activation as {@link getting-started/licensing/license-and-legal open source software under the GPL license}. It will then {@link getting-started/setup/managing-ckeditor-logo display a small "Powered by CKEditor" logo} in the editor area. + + For commercial purposes, there are {@link getting-started/licensing/license-and-legal trial, development and production license keys} are available. ## Obtaining a license diff --git a/docs/getting-started/licensing/plans.md b/docs/getting-started/licensing/plans.md index 123f33ecfc0..62742ccab67 100644 --- a/docs/getting-started/licensing/plans.md +++ b/docs/getting-started/licensing/plans.md @@ -7,5 +7,8 @@ order: 20 modified_at: 2024-06-25 --- -# Configuring CKEditor 5 features +# Available commercial CKEditor 5 license plans +Currently available plans: + +* list diff --git a/docs/umberto.json b/docs/umberto.json index 8d7674413dc..9ff5d233815 100644 --- a/docs/umberto.json +++ b/docs/umberto.json @@ -358,7 +358,7 @@ "tutorials/widgets/data-from-external-source.html": "framework/tutorials/widgets/data-from-external-source.html", "support/licensing/license-and-legal.html": "getting-started/licensing/license-and-legal.html", "support/licensing/license-key-and-activation.html": "getting-started/licensing/license-key-and-activation.html", - "getting-started/setup/license-key-and-activation.html": "getting-started/licensing/license-key-and-activation.html", + "getting-started/licensing/license-key-and-activation.html": "getting-started/licensing/license-key-and-activation.html", "support/licensing/managing-ckeditor-logo.html": "getting-started/setup/managing-ckeditor-logo.html", "support/reporting-issues.html": "support/index.html#reporting-issues", "support/license-and-legal": "getting-started/licensing/license-and-legal", From 697c2dd7013fe0b725966e5b42c22266dc45c532 Mon Sep 17 00:00:00 2001 From: godai78 Date: Fri, 19 Jul 2024 08:02:15 +0200 Subject: [PATCH 073/256] Docs: adding plans. [short flow] --- docs/getting-started/licensing/plans.md | 86 ++++++++++++++++++++++++- 1 file changed, 84 insertions(+), 2 deletions(-) diff --git a/docs/getting-started/licensing/plans.md b/docs/getting-started/licensing/plans.md index 62742ccab67..d8b0c7e30c8 100644 --- a/docs/getting-started/licensing/plans.md +++ b/docs/getting-started/licensing/plans.md @@ -9,6 +9,88 @@ modified_at: 2024-06-25 # Available commercial CKEditor 5 license plans -Currently available plans: +There are several licensing plans you can choose from for greater flexibility and value. -* list +## Free plan + +This is the ultimate cloud-hosted solution of open-source users. + +* 1,000 editor loads per month +* GPL2+ license +* Community support + +The plan includes: + +* Core Editing +* Mentions +* Media Embed +* Markdown Input and Output +* Page Management + +Add-ons available in this plan: CKBox + +## Essential plan + +Great cloud-hosted solution for small projects. + +* 5,000 editor loads per month +* Commercial license +* Professional Support (1 request/mo) + +Includes everything in Free, plus the following premium features: + +* Multi-level Lists +* Export to Word (200/each/month) +* Export to PDF (200/each/month) +* Advanced Productivity including: + * Case Change + * Enhanced Paste from Word + * Enhanced Paste from Excel + * Format Painter + * Merge Tags + * Slash Commands + * Templates +* Advanced Page Management + * Document Outline + * Table of Contents + +Add-ons available in this plan: CKBox + +## Professional plan + +Provides advanced functions needed in large projects. + +* 20,000 editor loads per month +* Commercial license +* Professional Support (4 requests/mo) + +Includes everything in the Free and Essential plans, plus: + +* Asynchronous or Real Time Collaboration + * Comments + * Track Changes + * Revision History + * Co-Authoring +* Import from Word (1,000/each/month) +* Export to Word (1,000/each/month) +* Export to PDF (1,000/each/month) + +Add-ons available in this plan: AI Assistant, CKBox + +## Custom plan + +If you nee more flexibility to suit your specific needs, let us tailor a solution for you. + +* Alternative licensing metrics +* Commercial license +* Enterprise Support with unlimited requests +* Dedicated Account Manager and Customer Care Coordinator +* Custom contract language available +* Reseller-based transactions. + +Custom plans have the option of: + +* Your selection of any of the entire catalog of 100+ CKEditor features. +* Hosting and deployment flexibility + + From 53ce0163aa36b7e6f3207076dfca8cf19733d203 Mon Sep 17 00:00:00 2001 From: godai78 Date: Fri, 19 Jul 2024 08:21:41 +0200 Subject: [PATCH 074/256] Docs: trial period update. [short flow] --- docs/features/productivity-pack.md | 2 +- docs/getting-started/legacy-getting-started/quick-start.md | 2 +- docs/getting-started/licensing/license-key-and-activation.md | 2 +- docs/getting-started/quick-start.md | 2 +- docs/getting-started/setup/license-key-and-activation.md | 2 +- packages/ckeditor5-ckbox/docs/features/ckbox.md | 4 ++-- packages/ckeditor5-ckfinder/docs/features/ckfinder.md | 2 +- packages/ckeditor5-easy-image/docs/features/easy-image.md | 2 +- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/features/productivity-pack.md b/docs/features/productivity-pack.md index 31754fc1d06..19660517b34 100644 --- a/docs/features/productivity-pack.md +++ b/docs/features/productivity-pack.md @@ -51,7 +51,7 @@ The productivity pack is a set of exclusive premium features for the CKEditor&nb ## Free trial -Before committing, you may try out the productivity pack, just like all CKEditor 5 premium features. To do it, start your non-commitment [30-day free trial](https://orders.ckeditor.com/trial/premium-features). +Before committing, you may try out the productivity pack, just like all CKEditor 5 premium features. To do it, start your non-commitment [14-day free trial](https://orders.ckeditor.com/trial/premium-features). Refer to the {@link @trial guides/standalone/productivity-pack productivity pack trial} guide for information on how to start using these features in your CKEditor 5 WYSIWYG editor now. diff --git a/docs/getting-started/legacy-getting-started/quick-start.md b/docs/getting-started/legacy-getting-started/quick-start.md index dcdca820ec7..2e277c4258e 100644 --- a/docs/getting-started/legacy-getting-started/quick-start.md +++ b/docs/getting-started/legacy-getting-started/quick-start.md @@ -306,7 +306,7 @@ While the superbuild is designed to provide as many of them as possible, some of ## Running a full-featured editor with Premium features -If you would like to quickly evaluate CKEditor 5 with premium features such as real-time collaboration, track changes, and revision history, sign up for a [30-day free trial](https://orders.ckeditor.com/trial/premium-features). +If you would like to quickly evaluate CKEditor 5 with premium features such as real-time collaboration, track changes, and revision history, sign up for a [14-day free trial](https://orders.ckeditor.com/trial/premium-features). After you sign up, in the customer dashboard you will find the full code snippet to run the editor with premium features with all the necessary configurations. diff --git a/docs/getting-started/licensing/license-key-and-activation.md b/docs/getting-started/licensing/license-key-and-activation.md index 2e5b3b855de..598a23438ad 100644 --- a/docs/getting-started/licensing/license-key-and-activation.md +++ b/docs/getting-started/licensing/license-key-and-activation.md @@ -43,7 +43,7 @@ If you wish to purchase a commercial CKEditor 5 license or a license to one ### Subscribing to the CKEditor Premium Features free trial -If you wish to test our offer, you can create an account by [signing up for CKEditor Premium Features 30-day free trial](https://orders.ckeditor.com/trial/premium-features). After signing up, you will receive access to the customer dashboard (CKEditor Ecosystem dashboard). +If you wish to test our offer, you can create an account by [signing up for CKEditor Premium Features 14-day free trial](https://orders.ckeditor.com/trial/premium-features). After signing up, you will receive access to the customer dashboard (CKEditor Ecosystem dashboard). The trial is commitment-free, and there is no need to provide credit card details to start it. The Premium Features free trial allows you to test all paid CKEditor Ecosystem products at no cost. diff --git a/docs/getting-started/quick-start.md b/docs/getting-started/quick-start.md index ccca04f8ec1..c1a95ed4223 100644 --- a/docs/getting-started/quick-start.md +++ b/docs/getting-started/quick-start.md @@ -420,7 +420,7 @@ All three installation methods – npm, CDN, ZIP – work similarly. So, ### Obtaining a license key -To activate CKEditor 5 premium features, you will need a commercial license. The easiest way to get one is to sign up for the [CKEditor Premium Features 30-day free trial](https://orders.ckeditor.com/trial/premium-features) to test the premium features. +To activate CKEditor 5 premium features, you will need a commercial license. The easiest way to get one is to sign up for the [CKEditor Premium Features 14-day free trial](https://orders.ckeditor.com/trial/premium-features) to test the premium features. You can also [contact us](https://ckeditor.com/contact/?sales=true#contact-form) to receive an offer tailored to your needs. To obtain an activation key, please follow the {@link getting-started/licensing/license-key-and-activation License key and activation} guide. diff --git a/docs/getting-started/setup/license-key-and-activation.md b/docs/getting-started/setup/license-key-and-activation.md index cc093d0b200..8d581243774 100644 --- a/docs/getting-started/setup/license-key-and-activation.md +++ b/docs/getting-started/setup/license-key-and-activation.md @@ -41,7 +41,7 @@ If you wish to purchase a commercial CKEditor 5 license or a license to one ### Subscribing to the CKEditor Premium Features free trial -If you wish to test our offer, you can create an account by [signing up for CKEditor Premium Features 30-day free trial](https://orders.ckeditor.com/trial/premium-features). After signing up, you will receive access to the customer dashboard (CKEditor Ecosystem dashboard). +If you wish to test our offer, you can create an account by [signing up for CKEditor Premium Features 14-day free trial](https://orders.ckeditor.com/trial/premium-features). After signing up, you will receive access to the customer dashboard (CKEditor Ecosystem dashboard). The trial is commitment-free, and there is no need to provide credit card details to start it. The Premium Features free trial allows you to test all paid CKEditor Ecosystem products at no cost. diff --git a/packages/ckeditor5-ckbox/docs/features/ckbox.md b/packages/ckeditor5-ckbox/docs/features/ckbox.md index 72974b56591..4d01da5133d 100644 --- a/packages/ckeditor5-ckbox/docs/features/ckbox.md +++ b/packages/ckeditor5-ckbox/docs/features/ckbox.md @@ -20,7 +20,7 @@ CKBox is a dedicated asset manager supporting file and image upload. The CKBox f * You need the **on-premises (self-hosted)** version of the service. * You have other licensing questions. - You can also sign up for the [CKEditor Premium Features 30-day free trial](https://orders.ckeditor.com/trial/premium-features) to test the feature. + You can also sign up for the [CKEditor Premium Features 14-day free trial](https://orders.ckeditor.com/trial/premium-features) to test the feature. ## How CKBox enhances CKEditor 5 @@ -131,7 +131,7 @@ The feature can be configured via the {@link module:ckbox/ckboxconfig~CKBoxConfi This is a premium feature. [Contact us](https://ckeditor.com/contact/?sales=true#contact-form) to receive an offer tailored to your needs. - You can also sign up for the [CKEditor Premium Features 30-day free trial](https://orders.ckeditor.com/trial/premium-features) to test the feature. + You can also sign up for the [CKEditor Premium Features 14-day free trial](https://orders.ckeditor.com/trial/premium-features) to test the feature. If you already have a valid license, please log into your [user dashboard](https://dashboard.ckeditor.com/) to access the feature settings. diff --git a/packages/ckeditor5-ckfinder/docs/features/ckfinder.md b/packages/ckeditor5-ckfinder/docs/features/ckfinder.md index 5d4d044e2e6..1af92a406d7 100644 --- a/packages/ckeditor5-ckfinder/docs/features/ckfinder.md +++ b/packages/ckeditor5-ckfinder/docs/features/ckfinder.md @@ -16,7 +16,7 @@ The CKFinder feature lets you insert images and links to files into your content This is a premium feature and you need a license for it on top of your CKEditor 5 commercial license. [Contact us](https://ckeditor.com/contact/?sales=true#contact-form) to receive an offer tailored to your needs. - You can also sign up for the [CKEditor Premium Features 30-day free trial](https://orders.ckeditor.com/trial/premium-features) to test the feature. + You can also sign up for the [CKEditor Premium Features 14-day free trial](https://orders.ckeditor.com/trial/premium-features) to test the feature. ## Demos diff --git a/packages/ckeditor5-easy-image/docs/features/easy-image.md b/packages/ckeditor5-easy-image/docs/features/easy-image.md index aa91b5a4927..5e75880f98a 100644 --- a/packages/ckeditor5-easy-image/docs/features/easy-image.md +++ b/packages/ckeditor5-easy-image/docs/features/easy-image.md @@ -13,7 +13,7 @@ The [Easy Image](https://ckeditor.com/ckeditor-cloud-services/easy-image/) is an This is a premium feature and you need a license for it on top of your CKEditor 5 commercial license. [Contact us](https://ckeditor.com/contact/?sales=true#contact-form) to receive an offer tailored to your needs. - You can also sign up for the [CKEditor Premium Features 30-day free trial](https://orders.ckeditor.com/trial/premium-features) to test the feature. + You can also sign up for the [CKEditor Premium Features 14-day free trial](https://orders.ckeditor.com/trial/premium-features) to test the feature. ## Demo From f93806830fdae1a839a23f06c45f25e8e23596ee Mon Sep 17 00:00:00 2001 From: godai78 Date: Fri, 19 Jul 2024 10:48:17 +0200 Subject: [PATCH 075/256] Docs: main guide draft. [short flow] --- docs/getting-started/licensing/plans.md | 6 +++--- docs/getting-started/licensing/ubb.md | 21 ++++++++++++++++++--- docs/getting-started/quick-start.md | 4 ++-- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/docs/getting-started/licensing/plans.md b/docs/getting-started/licensing/plans.md index d8b0c7e30c8..5fd31c941be 100644 --- a/docs/getting-started/licensing/plans.md +++ b/docs/getting-started/licensing/plans.md @@ -1,13 +1,13 @@ --- category: licensing -menu-title: Available plans -meta-title: Configuring editor features | CKEditor 5 documentation +menu-title: License plans +meta-title: Available license plans | CKEditor 5 documentation meta-description: Learn how to configure CKEditor 5. order: 20 modified_at: 2024-06-25 --- -# Available commercial CKEditor 5 license plans +# Available CKEditor 5 license plans There are several licensing plans you can choose from for greater flexibility and value. diff --git a/docs/getting-started/licensing/ubb.md b/docs/getting-started/licensing/ubb.md index de270ebd0ad..d0dcc8df13f 100644 --- a/docs/getting-started/licensing/ubb.md +++ b/docs/getting-started/licensing/ubb.md @@ -1,10 +1,25 @@ --- category: licensing menu-title: Usage-based billing -meta-title: Configuring editor features | CKEditor 5 documentation -meta-description: Learn how to configure CKEditor 5. +meta-title: Usage-based billing | CKEditor 5 documentation +meta-description: Learn how usage-based billing works in CKEditor 5. order: 30 modified_at: 2024-06-25 --- -# Configuring CKEditor 5 features +# Usage-based billing + +Usage-Based Billing (UBB) means the cost you incur depends on how much you use the TinyMCE editor. + +## How it works + +We offer {@link getting-started/licensing/plans several pricing plans for commercial users}. Each of these plans includes a specific number of editor loads. + + + An editor load is a term used for each time CKEditor 5 is loaded. For example, if 100 users load CKEditor 10 times each, there are 1,000 editor loads. + + +The number of editor loads needed depends on your application, however, all our plans have been developed to take into consideration actual use on our cloud infrastructure. + +If you require additional editor loads, you have the option to upgrade to a more suitable plan or pay for every block of 1,000 editor loads over your allocated plan limit. + diff --git a/docs/getting-started/quick-start.md b/docs/getting-started/quick-start.md index c1a95ed4223..408ca188034 100644 --- a/docs/getting-started/quick-start.md +++ b/docs/getting-started/quick-start.md @@ -214,7 +214,7 @@ The easiest way to see the editor in action is to serve the `index.html` file vi You must run your code on a local server to use import maps. Opening the HTML file directly in your browser will trigger security rules. These rules (CORS policy) ensure loading modules from the same source. Therefore, set up a local server, like `nginx`, `caddy`, `http-server`, to serve your files over HTTP or HTTPS. -All three installation methods – npm, CDN, ZIP – work similarly. So, you can also use the [CKEditor 5 Builder](https://ckeditor.com/ckeditor-5/builder/) with a ZIP archive. Create a custom preset with the Builder and combine it with the editor loaded from ZIP files. +All three installation methods – npm, CDN, ZIP – work similarly. It means, you can also use the [CKEditor 5 Builder](https://ckeditor.com/ckeditor-5/builder/) with a ZIP archive. Create a custom preset with the Builder and combine it with the editor loaded from ZIP files. ## Installing premium features @@ -416,7 +416,7 @@ The easiest way to see the editor in action is to serve the `index.html` file vi You must run your code on a local server to use import maps. Opening the HTML file directly in your browser will trigger security rules. These rules (CORS policy) ensure loading modules from the same source. Therefore, set up a local server, like `nginx`, `caddy`, `http-server`, to serve your files over HTTP or HTTPS. -All three installation methods – npm, CDN, ZIP – work similarly. So, you can also use the [CKEditor 5 Builder](https://ckeditor.com/ckeditor-5/builder/) with a ZIP archive. Create a custom preset with the Builder and combine it with the editor loaded from ZIP files. +All three installation methods – npm, CDN, ZIP – work similarly. It means, you can also use the [CKEditor 5 Builder](https://ckeditor.com/ckeditor-5/builder/) with a ZIP archive. Create a custom preset with the Builder and combine it with the editor loaded from ZIP files. ### Obtaining a license key From cd5f2acaef930f48f5f734cba591795ae759067f Mon Sep 17 00:00:00 2001 From: godai78 Date: Wed, 24 Jul 2024 08:14:38 +0200 Subject: [PATCH 076/256] Docs: a typo. [short flow] --- docs/getting-started/licensing/plans.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting-started/licensing/plans.md b/docs/getting-started/licensing/plans.md index 5fd31c941be..9185c77102f 100644 --- a/docs/getting-started/licensing/plans.md +++ b/docs/getting-started/licensing/plans.md @@ -79,7 +79,7 @@ Add-ons available in this plan: AI Assistant, CKBox ## Custom plan -If you nee more flexibility to suit your specific needs, let us tailor a solution for you. +If you need more flexibility to suit your specific needs, let us tailor a solution for you. * Alternative licensing metrics * Commercial license From a41202ea9d320ffa9077d4ea80f1e8d17c966ef0 Mon Sep 17 00:00:00 2001 From: godai78 Date: Fri, 26 Jul 2024 12:46:01 +0200 Subject: [PATCH 077/256] Docs: moving the PBC guide. [skip ci] --- docs/getting-started/licensing/license-key-and-activation.md | 4 ++-- .../{setup => licensing}/managing-ckeditor-logo.md | 4 ++-- docs/getting-started/licensing/plans.md | 2 +- docs/getting-started/licensing/ubb.md | 2 +- docs/getting-started/setup/license-key-and-activation.md | 2 +- docs/updating/update-to-38.md | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) rename docs/getting-started/{setup => licensing}/managing-ckeditor-logo.md (99%) diff --git a/docs/getting-started/licensing/license-key-and-activation.md b/docs/getting-started/licensing/license-key-and-activation.md index 598a23438ad..bfda02854e0 100644 --- a/docs/getting-started/licensing/license-key-and-activation.md +++ b/docs/getting-started/licensing/license-key-and-activation.md @@ -1,6 +1,6 @@ --- category: licensing -order: 15 +order: 20 meta-title: License key and activation | CKEditor 5 Documentation menu-title: License key and activation --- @@ -28,7 +28,7 @@ This article explains how to activate a commercial license of CKEditor 5 an Other premium features such as {@link features/real-time-collaboration real-time collaboration}, {@link features/export-word export to Word}, {@link features/export-pdf export to PDF}, or {@link features/import-word import from Word} are authenticated on the server side. Please refer to respective feature guides for installation details. - CKEditor 5 (without premium features listed above) can be used without activation as {@link getting-started/licensing/license-and-legal open source software under the GPL license}. It will then {@link getting-started/setup/managing-ckeditor-logo display a small "Powered by CKEditor" logo} in the editor area. + CKEditor 5 (without premium features listed above) can be used without activation as {@link getting-started/licensing/license-and-legal open source software under the GPL license}. It will then {@link getting-started/licensing/managing-ckeditor-logo display a small "Powered by CKEditor" logo} in the editor area. For commercial purposes, there are {@link getting-started/licensing/license-and-legal trial, development and production license keys} are available. diff --git a/docs/getting-started/setup/managing-ckeditor-logo.md b/docs/getting-started/licensing/managing-ckeditor-logo.md similarity index 99% rename from docs/getting-started/setup/managing-ckeditor-logo.md rename to docs/getting-started/licensing/managing-ckeditor-logo.md index 138b7df3278..a2aa208c71f 100644 --- a/docs/getting-started/setup/managing-ckeditor-logo.md +++ b/docs/getting-started/licensing/managing-ckeditor-logo.md @@ -1,6 +1,6 @@ --- -category: setup -order: 70 +category: licensing +order: 50 meta-title: Managing the "Powered by CKEditor" logo | CKEditor 5 Documentation meta-description: Managing the "Powered by CKEditor" logo --- diff --git a/docs/getting-started/licensing/plans.md b/docs/getting-started/licensing/plans.md index 9185c77102f..2095fe6d3b3 100644 --- a/docs/getting-started/licensing/plans.md +++ b/docs/getting-started/licensing/plans.md @@ -3,7 +3,7 @@ category: licensing menu-title: License plans meta-title: Available license plans | CKEditor 5 documentation meta-description: Learn how to configure CKEditor 5. -order: 20 +order: 30 modified_at: 2024-06-25 --- diff --git a/docs/getting-started/licensing/ubb.md b/docs/getting-started/licensing/ubb.md index d0dcc8df13f..3fea4b9c729 100644 --- a/docs/getting-started/licensing/ubb.md +++ b/docs/getting-started/licensing/ubb.md @@ -3,7 +3,7 @@ category: licensing menu-title: Usage-based billing meta-title: Usage-based billing | CKEditor 5 documentation meta-description: Learn how usage-based billing works in CKEditor 5. -order: 30 +order: 40 modified_at: 2024-06-25 --- diff --git a/docs/getting-started/setup/license-key-and-activation.md b/docs/getting-started/setup/license-key-and-activation.md index 8d581243774..b1360b25555 100644 --- a/docs/getting-started/setup/license-key-and-activation.md +++ b/docs/getting-started/setup/license-key-and-activation.md @@ -28,7 +28,7 @@ This article explains how to activate a commercial license of CKEditor 5 an Other premium features such as {@link features/real-time-collaboration real-time collaboration}, {@link features/export-word export to Word}, {@link features/export-pdf export to PDF}, or {@link features/import-word import from Word} are authenticated on the server side. Please refer to respective feature guides for installation details. - CKEditor 5 (without premium features listed above) can be used without activation as {@link support/license-and-legal open source software under the GPL license}. It will then {@link getting-started/setup/managing-ckeditor-logo display a small "Powered by CKEditor" logo} in the editor area. + CKEditor 5 (without premium features listed above) can be used without activation as {@link support/license-and-legal open source software under the GPL license}. It will then {@link getting-started/licensing/managing-ckeditor-logo display a small "Powered by CKEditor" logo} in the editor area. ## Obtaining a license diff --git a/docs/updating/update-to-38.md b/docs/updating/update-to-38.md index 6d8096718a4..10ce8f5da9b 100644 --- a/docs/updating/update-to-38.md +++ b/docs/updating/update-to-38.md @@ -45,7 +45,7 @@ Starting from version 38.0.0, all **open source installations** of CKEditor  If you have a **commercial license**, you can hide the logo by adding {@link module:core/editor/editorconfig~EditorConfig#licenseKey `config.licenseKey`} to your configuration. If you already use pagination, productivity pack, or asynchronous collaboration features, you do not need to take any action as you should already have `config.licenseKey` in place. The logo will not be visible in your editor. -We have prepared a detailed {@link getting-started/setup/managing-ckeditor-logo Managing the "Powered by CKEditor" logo} guide to help everyone through the transition and explain any concerns. +We have prepared a detailed {@link getting-started/licensing/managing-ckeditor-logo Managing the "Powered by CKEditor" logo} guide to help everyone through the transition and explain any concerns. ### Introduction of color pickers to font color and font background color features From 733179c72aa698b0d9d1ad54d42ff932ae648fb5 Mon Sep 17 00:00:00 2001 From: godai78 Date: Fri, 26 Jul 2024 13:43:15 +0200 Subject: [PATCH 078/256] Docs: implementing new structure; dummy guides. [short flow] --- .../integrations-cdn/angular.md | 697 ++++++++++++++++++ .../integrations-cdn/dotnet.md | 223 ++++++ .../integrations-cdn/laravel.md | 220 ++++++ .../integrations-cdn/next-js.md | 117 +++ .../integrations-cdn/react-multiroot.md | 143 ++++ .../getting-started/integrations-cdn/react.md | 284 +++++++ .../integrations-cdn/vuejs-v2.md | 452 ++++++++++++ .../integrations-cdn/vuejs-v3.md | 480 ++++++++++++ docs/getting-started/integrations/angular.md | 2 +- docs/getting-started/integrations/dotnet.md | 2 +- docs/getting-started/integrations/laravel.md | 2 +- docs/getting-started/integrations/next-js.md | 4 +- .../integrations/react-multiroot.md | 2 +- docs/getting-started/integrations/react.md | 2 +- docs/getting-started/integrations/vuejs-v2.md | 2 +- docs/getting-started/integrations/vuejs-v3.md | 2 +- .../setup/integrations/angular.md | 697 ++++++++++++++++++ .../setup/integrations/react.md | 284 +++++++ .../setup/integrations/vuejs-v3.md | 480 ++++++++++++ docs/umberto.json | 40 +- 20 files changed, 4121 insertions(+), 14 deletions(-) create mode 100644 docs/getting-started/integrations-cdn/angular.md create mode 100644 docs/getting-started/integrations-cdn/dotnet.md create mode 100644 docs/getting-started/integrations-cdn/laravel.md create mode 100644 docs/getting-started/integrations-cdn/next-js.md create mode 100644 docs/getting-started/integrations-cdn/react-multiroot.md create mode 100644 docs/getting-started/integrations-cdn/react.md create mode 100644 docs/getting-started/integrations-cdn/vuejs-v2.md create mode 100644 docs/getting-started/integrations-cdn/vuejs-v3.md create mode 100644 docs/getting-started/setup/integrations/angular.md create mode 100644 docs/getting-started/setup/integrations/react.md create mode 100644 docs/getting-started/setup/integrations/vuejs-v3.md diff --git a/docs/getting-started/integrations-cdn/angular.md b/docs/getting-started/integrations-cdn/angular.md new file mode 100644 index 00000000000..001b6b773f0 --- /dev/null +++ b/docs/getting-started/integrations-cdn/angular.md @@ -0,0 +1,697 @@ +--- +menu-title: Angular +meta-title: Angular rich text editor component | CKEditor 5 documentation +category: cloud +order: 30 +--- + +{@snippet installation/integrations/framework-integration} + +# Angular rich text editor component + +

+ + npm version + +

+ +Angular is a TypeScript-based, open-source, single-page web application framework. The CKEditor 5 component for Angular supports integrating different editor types. + + + Starting from version 6.0.0 of this package, you can use native type definitions provided by CKEditor 5. Check the details about {@link getting-started/setup/typescript-support TypeScript support}. + + +## Supported Angular versions + +Because of the breaking changes in the Angular library output format, the `@ckeditor/ckeditor5-angular` package is released in the following versions to support various Angular ecosystems: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CKEditor 5  Angular component versionAngular versionDetails
Actively supported versions
^813+Requires CKEditor 5 in version 42 or higher.
Past releases (no longer maintained)
^713+Changes in peer dependencies (issue). Requires CKEditor 5 in version 37 or higher.
^613+Requires CKEditor 5 in version 37 or higher.
^513+Requires Angular in version 13+ or higher. Lower versions are no longer maintained.
^513+Requires Angular in version 13+ or higher. Lower versions are no longer maintained.
^49.1+Requires CKEditor 5 in version 34 or higher.
^39.1+Requires Node.js in version 14 or higher.
^29.1+Migration to TypeScript 4. Declaration files are not backward compatible.
^15.x - 8.xAngular versions no longer maintained.
+ +All available Angular versions are [listed on npm](https://www.npmjs.com/package/@ckeditor/ckeditor5-angular), where they can be pulled from. + +## Quick start + +### Using CKEditor 5 Builder + +The easiest way to use CKEditor 5 in your Angular application is by configuring it with [CKEditor 5 Builder](https://ckeditor.com/builder?redirect=docs) and integrating it with your application. Builder offers an easy-to-use user interface to help you configure, preview, and download the editor suited to your needs. You can easily select: + +* the features you need, +* the preferred framework (React, Angular, Vue or Vanilla JS), +* the preferred distribution method. + +You get ready-to-use code tailored to your needs! + +### Setting up the project + +This guide assumes you already have a Angular project. To create such a project, you can use Angular CLI. Refer to the [Angular documentation](https://angular.io/cli) to learn more. + +### Installing from npm + +First, install the CKEditor 5 packages: + +* `ckeditor5` – package with open-source plugins and features. +* `ckeditor5-premium-features` – package with premium plugins and features. + +Depending on your configuration and chosen plugins, you may need to install the first or both packages. + +```bash +npm install ckeditor5 ckeditor5-premium-features +``` + +Then, install the [CKEditor 5 WYSIWYG editor component for Angular](https://www.npmjs.com/package/@ckeditor/ckeditor5-angular): + +```bash +npm install @ckeditor/ckeditor5-angular +``` + +The following setup differs depending on the type of components you use. + +#### Standalone components + +Standalone components provide a simplified way to build Angular applications. They are enabled in Angular 17 by default. Standalone components aim to simplify the setup and reduce the need for `NGModules`. That is why you do not need such a module in this case. + +Instead, add the `CKEditorModule` to the imports in your app component. The component needs the `standalone` option set to `true`. The example below shows how to use the component with open-source and premium plugins. + +```ts +// app.component.ts + +import { Component, ViewEncapsulation } from '@angular/core'; +import { CKEditorModule } from '@ckeditor/ckeditor5-angular'; +import { ClassicEditor, Bold, Essentials, Italic, Mention, Paragraph, Undo } from 'ckeditor5'; +import { SlashCommand } from 'ckeditor5-premium-features'; + +@Component( { + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.css'], + encapsulation: ViewEncapsulation.None, + imports: [ CKEditorModule ], + standalone: true +} ) +export class AppComponent { + title = 'angular'; + + public Editor = ClassicEditor; + public config = { + toolbar: [ 'undo', 'redo', '|', 'bold', 'italic' ], + plugins: [ + Bold, Essentials, Italic, Mention, Paragraph, SlashCommand, Undo + ], + licenseKey: '', + // mention: { + // Mention configuration + // } + } +} +``` + +Depending on the plugins used (open source only or premium too), you may need to import the first or both CSS files. Angular, by default, scopes styles to a particular component. Because of that, the editor may not detect attached styles. You must set the encapsulation option to `ViewEncapsulation.None` to turn this scoping off. + +```css +/* app.component.css */ + +@import 'ckeditor5/ckeditor5.css'; +@import 'ckeditor5-premium-features/ckeditor5-premium-features.css'; +``` + +Then, use the `` tag in the template to run the rich text editor: + +```html + + + +``` + +#### NGModule components + +If you want to use NGModule components, add the `CKEditorModule` to the `imports` array. It will make the CKEditor 5 component available in your Angular application. + +```ts +// app.module.ts + +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { CKEditorModule } from '@ckeditor/ckeditor5-angular'; + +import { AppComponent } from './app.component'; + +@NgModule( { + declarations: [ + AppComponent + ], + imports: [ + BrowserModule, + CKEditorModule + ], + providers: [], + bootstrap: [ AppComponent ] +} ) +export class AppModule { } +``` + +Then, import the editor in your Angular component and assign it to a `public` property to make it accessible from the template. The below example shows how to use the component with open-source and premium plugins. + +```ts +// app.component.ts + +import { Component, ViewEncapsulation } from '@angular/core'; +import { ClassicEditor, Bold, Essentials, Italic, Mention, Paragraph, Undo } from 'ckeditor5'; +import { SlashCommand } from 'ckeditor5-premium-features'; + +@Component( { + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: [ './app.component.css' ], + encapsulation: ViewEncapsulation.None +} ) +export class AppComponent { + title = 'angular'; + + public Editor = ClassicEditor; + public config = { + toolbar: [ 'undo', 'redo', '|', 'bold', 'italic' ], + plugins: [ + Bold, Essentials, Italic, Mention, Paragraph, SlashCommand, Undo + ], + licenseKey: '', + // mention: { + // Mention configuration + // } + } +} +``` + +Depending on the plugins you used, you may need to import the first or both CSS files. Angular, by default, scope styles to a particular component. Because of that, the editor may not detect attached styles. You must set the encapsulation option to `ViewEncapsulation.None` to turn this scoping off. + +```css +/* app.component.css */ + +@import 'ckeditor5/ckeditor5.css'; +@import 'ckeditor5-premium-features/ckeditor5-premium-features.css'; +``` + +Finally, use the `` tag in the template to run the rich text editor: + +```html + + + +``` + +## Supported `@Input` properties + +The following `@Input` properties are supported by the CKEditor 5 rich text editor component for Angular: + +### `editor` (required) + +The {@link getting-started/setup/editor-lifecycle `Editor`} which provides the static {@link module:core/editor/editor~Editor.create `create()`} method to create an instance of the editor: + +```html + +``` + +### `config` + +The {@link module:core/editor/editorconfig~EditorConfig configuration} of the editor: + +```html + +``` + +### `data` + +The initial data of the editor. It can be a static value: + +```html + +``` + +or a shared parent component's property + +```ts +@Component( { + // ... +} ) +export class MyComponent { + public editorData = '

Hello, world!

'; + // ... +} +``` + +```html + +``` + +### `tagName` + +The tag name of the HTML element on which the rich text editor will be created. + +The default tag is `
`. + +```html + +``` + +### `disabled` + +Controls the editor's {@link module:core/editor/editor~Editor#isReadOnly read–only} state: + +```ts +@Component( { + // ... +} ) +export class MyComponent { + public isDisabled = false; + // ... + toggleDisabled() { + this.isDisabled = !this.isDisabled + } +} +``` + +```html + + + +``` + + + +### `watchdog` + +An instance of the {@link module:watchdog/contextwatchdog~ContextWatchdog `ContextWatchdog`} class that is responsible for providing the same context to multiple editor instances and restarting the whole structure in case of crashes. + +```ts +import CKSource from 'path/to/custom/build'; + +const Context = CKSource.Context; +const Editor = CKSource.Editor; +const ContextWatchdog = CKSource.ContextWatchdog; + +@Component( { + // ... +} ) +export class MyComponent { + public editor = Editor; + public watchdog: any; + public ready = false; + + ngOnInit() { + const contextConfig = {}; + + this.watchdog = new ContextWatchdog( Context ); + + this.watchdog.create( contextConfig ) + .then( () => { + this.ready = true; + } ); + } +} +``` + +```html +
+ + + +
+``` + +### `editorWatchdogConfig` + +If the `watchdog` property is not used, {@link module:watchdog/editorwatchdog~EditorWatchdog `EditorWatchdog`} will be used by default. `editorWatchdogConfig` property allows for passing a {@link module:watchdog/watchdog~WatchdogConfig config} to that watchdog. + +```ts +@Component( { + // ... +} ) +export class MyComponent { + public myWatchdogConfig = { + crashNumberLimit: 5, + // ... + }; + // ... +} +``` + +```html + +``` + +### `disableTwoWayDataBinding` + +Allows disabling the two-way data binding mechanism. The default value is `false`. + +The reason for the introduction of this option are performance issues in large documents. By default, while using the `ngModel` directive, whenever the editor's data is changed, the component must synchronize the data between the editor instance and the connected property. This results in calling the {@link module:core/editor/editor~Editor#getData `editor.getData()`} function, which causes a massive slowdown while typing in large documents. + +This option allows the integrator to disable the default behavior and only call the {@link module:core/editor/editor~Editor#getData `editor.getData()`} method on demand, which prevents the slowdowns. You can read more in the [relevant issue](https://github.com/ckeditor/ckeditor5-angular/issues/141). + +## Supported `@Output` properties + +The following `@Output` properties are supported by the CKEditor 5 rich text editor component for Angular: + +### `ready` + +Fired when the editor is ready. It corresponds with the [`editor#ready`](https://ckeditor.com/docs/ckeditor5/latest/api/module_core_editor_editor-Editor.html#event-ready) event. +It is fired with the editor instance. + +Note that this method might be called multiple times. Apart from initialization, it is also called whenever the editor is restarted after a crash. Do not keep the reference to the editor instance internally, because it will change in case of restart. Instead, you should use `watchdog.editor` property. + +### `change` + +Fired when the content of the editor has changed. It corresponds with the {@link module:engine/model/document~Document#event:change:data `editor.model.document#change:data`} event. +It is fired with an object containing the editor and the CKEditor 5 `change:data` event object. + +```html + +``` + +```ts +import { ClassicEditor } from 'ckeditor5'; +import { ChangeEvent } from '@ckeditor/ckeditor5-angular/ckeditor.component'; + +@Component( { + // ... +} ) +export class MyComponent { + public Editor = ClassicEditor; + + public onChange( { editor }: ChangeEvent ) { + const data = editor.getData(); + + console.log( data ); + } + // ... +} +``` + +### `blur` + +Fired when the editing view of the editor is blurred. It corresponds with the {@link module:engine/view/document~Document#event:blur `editor.editing.view.document#blur`} event. +It is fired with an object containing the editor and the CKEditor 5 `blur` event data. + +### `focus` + +Fired when the editing view of the editor is focused. It corresponds with the {@link module:engine/view/document~Document#event:focus `editor.editing.view.document#focus`} event. +It is fired with an object containing the editor and the CKEditor 5 `focus` event data. + +### `error` + +Fired when the editor crashes. Once the editor is crashed, the internal watchdog mechanism restarts the editor and fires the [ready](#ready) event. + + + Prior to ckeditor5-angular `v7.0.1`, this event was not fired for crashes during the editor initialization. + + +## Integration with `ngModel` + +The component implements the [`ControlValueAccessor`](https://angular.io/api/forms/ControlValueAccessor) interface and works with the `ngModel`. Here is how to use it: + +Create some model in your component to share with the editor: + +```ts +@Component( { + // ... +} ) +export class MyComponent { + public model = { + editorData: '

Hello, world!

' + }; + // ... +} +``` + +Use the model in the template to enable a two–way data binding: + +```html + +``` + +### Styling + +The CKEditor 5 rich text editor component for Angular can be styled using the component style sheet or using a global style sheet. See how to set the CKEditor 5 component's height using these two approaches. + +### Setting the height via the component style sheet + +First, create a (S)CSS file in the parent component's directory and style the given editor's part preceded by the `:host` and `::ng-deep` pseudo selectors: + +```css +/* src/app/app.component.css */ + +:host ::ng-deep .ck-editor__editable_inline { + min-height: 500px; +} +``` + +Then in the parent component add the relative path to the above style sheet: + +```ts +/* src/app/app.component.ts */ + +@Component( { + // ... + styleUrls: [ './app.component.css' ] +} ) +``` + +#### Setting the height via a global style sheet + +To style the component using a global style sheet, first, create it: + +```css +/* src/styles.css */ + +.ck-editor__editable_inline { + min-height: 500px; +} +``` + +Then, add it in the `angular.json` configuration file: + +```json +"architect": { + "build": { + "options": { + "styles": [ + { "input": "src/styles.css" } + ] + } + } +} +``` + +#### Setting the placeholder + +To display {@link features/editor-placeholder the placeholder} in the main editable element, set the `placeholder` field in the CKEditor 5 rich text editor component configuration: + +```ts +@Component( { + // ... +} ) +export class MyComponent { + public config = { + placeholder: 'Type the content here!' + } +} +``` + +### Accessing the editor instance + +The CKEditor 5 rich text editor component provides all the functionality needed for most use cases. When access to the full CKEditor 5 API is needed you can get the editor instance with an additional step. + +To do this, create a template reference variable `#editor` pointing to the `` component: + +```html + +``` + +Then get the `` component using a property decorated by `@ViewChild( 'editor' )` and access the editor instance when needed: + +```ts +@Component() +export class MyComponent { + @ViewChild( 'editor' ) editorComponent: CKEditorComponent; + + public getEditor() { + // Warning: This may return "undefined" if the editor is hidden behind the `*ngIf` directive or + // if the editor is not fully initialised yet. + return this.editorComponent.editorInstance; + } +} +``` + + + The editor creation is asynchronous so the `editorInstance` will not be available until the editor is created. If you want to make changes to an editor that has just been created, a better option would be getting the CKEditor 5 instance on the [`ready`](#ready) event. + + +## How to? + +### Using the Document editor type + +If you want to use the {@link framework/document-editor document (decoupled) editor}, you need to {@link module:editor-decoupled/decouplededitor~DecoupledEditor.create add the toolbar to the DOM manually}: + +```ts +// app.component.ts + +import { Component, ViewEncapsulation } from '@angular/core'; +import { CKEditorModule } from '@ckeditor/ckeditor5-angular'; +import { DecoupledEditor, Essentials, Italic, Paragraph, Bold, Undo } from 'ckeditor5'; + +@Component( { + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: [ './app.component.css' ], + encapsulation: ViewEncapsulation.None + imports: [ CKEditorModule ], + standalone: true +} ) +export class AppComponent { + title = 'angular'; + + public Editor = DecoupledEditor; + public config = { + plugins: [ Bold, Essentials, Italic, Paragraph, Undo ], + toolbar: [ 'undo', 'redo', '|', 'bold', 'italic' ] + } + public onReady( editor: DecoupledEditor ): void { + const element = editor.ui.getEditableElement()!; + const parent = element.parentElement!; + + parent.insertBefore( + editor.ui.view.toolbar.element!, + element + ); + } +} +``` + +Import the needed CSS style sheet: + +```css +/* app.component.css */ + +@import 'ckeditor5/ckeditor5.css'; +``` + +And then, link the method in the template: + +```html + + + +``` + +### Using the editor with collaboration plugins + +We provide a few **ready-to-use integrations** featuring collaborative editing in Angular applications: + +* [CKEditor 5 with real-time collaboration features](https://github.com/ckeditor/ckeditor5-collaboration-samples/tree/master/real-time-collaboration-for-angular) +* [CKEditor 5 with real-time collaboration and revision history features](https://github.com/ckeditor/ckeditor5-collaboration-samples/tree/master/real-time-collaboration-revision-history-for-angular) +* [CKEditor 5 with the revision history feature](https://github.com/ckeditor/ckeditor5-collaboration-samples/tree/master/revision-history-for-angular) +* [CKEditor 5 with the track changes feature](https://github.com/ckeditor/ckeditor5-collaboration-samples/tree/master/track-changes-for-angular) + +It is not mandatory to build applications on top of the above samples, however, they should help you get started. + +### Localization + +CKEditor 5 supports multiple UI languages, and so does the official Angular component. Follow the instructions below to translate CKEditor 5 in your Angular application. + +Similarly to CSS style sheets, both packages have separate translations. Import them as shown in the example below. Then, pass them to the `translations` array of the `config` property. + +```ts +// app.component.ts + +import { Component, ViewEncapsulation } from '@angular/core'; +import { CKEditorModule } from '@ckeditor/ckeditor5-angular'; +import { ClassicEditor } from 'ckeditor5'; +// More imports... + +import coreTranslations from 'ckeditor5/translations/es.js'; +import premiumFeaturesTranslations from 'ckeditor5-premium-features/translations/es.js'; + +@Component( { + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: [ './app.component.css' ], + encapsulation: ViewEncapsulation.None + imports: [ CKEditorModule ], + standalone: true +} ) +export class AppComponent { + title = 'angular'; + public Editor = ClassicEditor; + public config = { + translations: [ coreTranslations, premiumFeaturesTranslations ], + // More configuration options... + } +} +``` + +For advanced usage see the {@link getting-started/setup/ui-language Setting the UI language} guide. + +## Contributing and reporting issues + +The source code of the CKEditor 5 rich text editor component for Angular is available on GitHub in [https://github.com/ckeditor/ckeditor5-angular](https://github.com/ckeditor/ckeditor5-angular). diff --git a/docs/getting-started/integrations-cdn/dotnet.md b/docs/getting-started/integrations-cdn/dotnet.md new file mode 100644 index 00000000000..5b0aa1d1efa --- /dev/null +++ b/docs/getting-started/integrations-cdn/dotnet.md @@ -0,0 +1,223 @@ +--- +category: cloud +meta-title: Compatibility with .NET | CKEditor 5 documentation +order: 80 +menu-title: .NET +--- + +# Compatibility with .NET + +As a pure JavaScript/TypeScript application, CKEditor 5 will work inside any environment that supports such components. While we do not offer official integrations for any non-JavaScript frameworks, you can include a custom configuration of CKEditor 5 in a non-JS framework of your choice, for example, Microsoft's [.NET](https://dotnet.microsoft.com/). + +## Using CKEditor 5 Builder + +The easiest way to use CKEditor 5 in your .NET project is preparing an editor preset with [CKEditor 5 Builder](https://ckeditor.com/builder?redirect=docs) and including it into your project. Builder offers an easy-to-use user interface to help you configure, preview, and download the editor suited to your needs. You can easily select: + +* the features you need, +* the preferred framework (React, Angular, Vue or Vanilla JS), +* the preferred distribution method. + +You get ready-to-use code tailored to your needs! + +## Setting up the project + +For the purpose of this guide, we will use a basic ASP.NET Core project created with `dotnet new webapp`. You can refer to the [ASP.NET Core documentation](https://learn.microsoft.com/en-us/aspnet/core/getting-started/?view=aspnetcore-7.0) to learn how to set up a project in the framework. + +## Integrating from CDN + +Once the project has been prepared, create an `assets/vendor/ckeditor5.js` file in the existing `wwwroot` directory in your app. Your folder structure should resemble this one: + +```plain +├── bin +├── obj +├── Pages +│ ├── Index.cshtml +│ └── ... +├── Properties +├── wwwroot +│ ├── assets +| ├── vendor +| └── ckeditor5.js +│ ├── css +│ ├── js +│ ├── lib +│ └── favicon.ico +├── appsettings.Development.json +├── appsettings.json +└── ... +``` + +Inside the file, paste the JavaScript code from CKEditor 5 Builder. The code will differ depending on your chosen preset and features. But it should look similar to this: + +```js +import { + ClassicEditor, + AccessibilityHelp, + Autosave, + Bold, + Essentials, + Italic, + Mention, + Paragraph, + SelectAll, + Undo +} from 'ckeditor5'; +import { SlashCommand } from 'ckeditor5-premium-features'; + +const editorConfig = { + toolbar: { + items: ['undo', 'redo', '|', 'selectAll', '|', 'bold', 'italic', '|', 'accessibilityHelp'], + shouldNotGroupWhenFull: false + }, + placeholder: 'Type or paste your content here!', + plugins: [AccessibilityHelp, Autosave, Bold, Essentials, Italic, Mention, Paragraph, SelectAll, SlashCommand, Undo], + licenseKey: '', + mention: { + feeds: [ + { + marker: '@', + feed: [ + /* See: https://ckeditor.com/docs/ckeditor5/latest/features/mentions.html */ + ] + } + ] + }, + initialData: "

Congratulations on setting up CKEditor 5! 🎉

" +}; + +ClassicEditor + .create( document.querySelector( '#editor' ), editorConfig ) + .then( editor => { + console.log( editor ); + } ) + .catch( error => { + console.error( error ); + } ); +``` + +Then, modify the `Index.cshtml` file in the `Pages` directory to include the CKEditor 5 scripts. All necessary scripts and links are in the HTML snippet from CKEditor 5 Builder. You can copy and paste them into your template. It should look similar to the one below: + +```html +@page +@model IndexModel +@{ + ViewData["Title"] = "Home page"; +} + +
+
+ + + + +
+``` + +Finally, in the root directory of your .NET project, run `dotnet watch run` to see the app in action. + +## Integrating using ZIP + + + Our new CKEditor 5 Builder does not provide ZIP output yet – but it will in the future. In the meantime, you can use one of the generic ZIP packages provided [on the download page](https://ckeditor.com/ckeditor-5/download/#zip). + + +After downloading and unpacking the ZIP archive, copy the `ckeditor5.js` and `ckeditor5.css` files in the `wwwroot/assets/vendor/` directory. The folder structure of your app should resemble this one. + +```plain +├── bin +├── obj +├── Pages +│ ├── Index.cshtml +│ └── ... +├── Properties +├── wwwroot +│ ├── assets +| ├── vendor +| ├── ckeditor5.js +| └── ckeditor5.css +│ ├── css +│ ├── js +│ ├── lib +│ └── favicon.ico +├── appsettings.Development.json +├── appsettings.json +└── ... +``` + +Having all the dependencies of CKEditor 5, modify the `Index.cshtml` file in the `Pages` directory to import them. All the necessary markup is in the `index.html` file from the ZIP archive. You can copy and paste it into your page. Pay attention to the paths of the import map and CSS link - they should reflect your folder structure. The template should look similar to the one below: + +```html +@page +@model IndexModel +@{ + ViewData["Title"] = "Home page"; +} + + + + + + + CKEditor 5 - Quick start ZIP + + + + +
+
+

Hello from CKEditor 5!

+
+
+ + + + +``` + +Finally, in the root directory of your .NET project, run `dotnet watch run` to see the app in action. diff --git a/docs/getting-started/integrations-cdn/laravel.md b/docs/getting-started/integrations-cdn/laravel.md new file mode 100644 index 00000000000..870ac80f5e1 --- /dev/null +++ b/docs/getting-started/integrations-cdn/laravel.md @@ -0,0 +1,220 @@ +--- +category: cloud +meta-title: Compatibility with Laravel | CKEditor 5 documentation +order: 70 +menu-title: Laravel +--- + +# Compatibility with Laravel + +As a pure JavaScript/TypeScript application, CKEditor 5 will work inside any environment that supports such components. While we do not offer official integrations for any non-JavaScript frameworks, you can include a custom configuration of CKEditor 5 in a non-JS framework of your choice, for example, the PHP-based [Laravel](https://laravel.com/). + +## Using CKEditor 5 Builder + +The easiest way to use CKEditor 5 in your Laravel project is preparing an editor preset with [CKEditor 5 Builder](https://ckeditor.com/builder?redirect=preset) and including it into your project. It offers an easy-to-use user interface to help you configure, preview, and download the editor suited to your needs. You can easily select: + +* the features you need, +* the preferred framework (React, Angular, Vue or Vanilla JS), +* the preferred distribution method. + +You get ready-to-use code tailored to your needs! + +## Setting up the project + +This guide assume you have a Laravel project. You can create a basic Laravel project using [Composer](https://getcomposer.org/). Refer to the [Laravel documentation](https://laravel.com/docs/10.x/installation) to learn how to set up a project in the framework. + +## Integrating from CDN + +Once the project has been prepared, create an `assets/vendor/ckeditor5.js` file in the existing `public` directory in your app. Your folder structure should resemble this one: + +```plain +├── app +├── bootstrap +├── config +├── database +├── public +│ ├── assets +| ├── vendor +| └── ckeditor5.js +│ ├── .htaccess +│ ├── favicon.ico +│ ├── index.php +│ └── robots.txt +├── resources +│ ├── views +| ├── welcome.blade.php +| └── ... +├── routes +└── ... +``` + +Inside the file, paste the JavaScript code from CKEditor 5 Builder. The code will differ depending on your chosen preset and features. But it should look similar to this: + +```js +import { + ClassicEditor, + AccessibilityHelp, + Autosave, + Bold, + Essentials, + Italic, + Mention, + Paragraph, + SelectAll, + Undo +} from 'ckeditor5'; +import { SlashCommand } from 'ckeditor5-premium-features'; + +const editorConfig = { + toolbar: { + items: ['undo', 'redo', '|', 'selectAll', '|', 'bold', 'italic', '|', 'accessibilityHelp'], + shouldNotGroupWhenFull: false + }, + placeholder: 'Type or paste your content here!', + plugins: [AccessibilityHelp, Autosave, Bold, Essentials, Italic, Mention, Paragraph, SelectAll, SlashCommand, Undo], + licenseKey: '', + mention: { + feeds: [ + { + marker: '@', + feed: [ + /* See: https://ckeditor.com/docs/ckeditor5/latest/features/mentions.html */ + ] + } + ] + }, + initialData: "

Congratulations on setting up CKEditor 5! 🎉

" +}; + +ClassicEditor + .create( document.querySelector( '#editor' ), editorConfig ) + .then( editor => { + console.log( editor ); + } ) + .catch( error => { + console.error( error ); + } ); +``` + +Then, modify the `welcome.blade.php` file in the `resources/views` directory to include the CKEditor 5 scripts. All necessary scripts and links are in the HTML snippet from CKEditor 5 Builder. You can copy and paste them into your template. It should look similar to the one below: + +```html + + + + + CKE5 in Laravel + + + + + + +
+ + +``` + +Finally, in the root directory of your Laravel project, run `php artisan serve` to see the app in action. + +## Integrating using ZIP + + + Our new CKEditor 5 Builder does not provide ZIP output yet – but it will in the future. In the meantime, you can use one of the generic ZIP packages provided [on the download page](https://ckeditor.com/ckeditor-5/download/#zip). + + +After downloading and unpacking the ZIP archive, copy the `ckeditor5.js` and `ckeditor5.css` files in the `public/assets/vendor/` directory. The folder structure of your app should resemble this one. + +```plain +├── app +├── bootstrap +├── config +├── database +├── public +│ ├── assets +| ├── vendor +| ├── ckeditor5.js +| └── ckeditor5.css +│ ├── .htaccess +│ ├── favicon.ico +│ ├── index.php +│ └── robots.txt +├── resources +│ ├── views +| ├── welcome.blade.php +| └── ... +├── routes +└── ... +``` + +Having all the dependencies of CKEditor 5, modify the `welcome.blade.php` file in the `resources/views` directory to import them. All the necessary markup is in the `index.html` file from the ZIP archive. You can copy and paste it into your template. Pay attention to the paths of the import map and CSS link – they should reflect your folder structure. The template should look similar to the one below: + +```html + + + + + + CKEditor 5 - Quick start ZIP + + + + +
+
+

Hello from CKEditor 5!

+
+
+ + + + +``` + +Finally, in the root directory of your Laravel project, run `php artisan serve` to see the app in action. diff --git a/docs/getting-started/integrations-cdn/next-js.md b/docs/getting-started/integrations-cdn/next-js.md new file mode 100644 index 00000000000..255e2fa06aa --- /dev/null +++ b/docs/getting-started/integrations-cdn/next-js.md @@ -0,0 +1,117 @@ +--- +menu-title: Next.js +meta-title: Integration with Next.js | CKEditor 5 documentation +meta-description: Learn how to integrate the rich text editor - CKEditor 5 - with the Next.js framework using the App Router or Pages Router routing strategies. +category: cloud +order: 40 +modified_at: 2023-11-14 +--- + +# Integrate CKEditor 5 with Next.js + +[Next.js](https://nextjs.org/) is a React meta-framework that helps create full-stack web applications. It offers different rendering strategies like server-side rendering (SSR), client-side rendering (CSR), or static site generation (SSG). Additionally, it provides file-based routing, automatic code splitting, and other handy features out of the box. + +Next.js 13 introduced a new App Router as an alternative to the previous Pages Router. App Router supports server components and is more server-centric than Pages Router, which is client-side oriented. + +CKEditor 5 does not support server-side rendering yet, but you can integrate it with the Next.js framework. In this guide, you will add the editor to a Next.js project using both routing paradigms. For this purpose, you will need [Next.js CLI](https://nextjs.org/docs/app/api-reference/create-next-app), and the official {@link getting-started/integrations/react CKEditor 5 React component}. + +## Using CKEditor 5 Builder + +The easiest way to use CKEditor 5 in your Next.js application is configuring it with [CKEditor 5 Builder](https://ckeditor.com/builder?redirect=docs) and integrating it with your project. Builder offers an easy-to-use user interface to help you configure, preview, and download the editor suited to your needs. You can easily select: + +* the features you need, +* the preferred framework (React, Angular, Vue or Vanilla JS), +* the preferred distribution method. + +You get ready-to-use code tailored to your needs! You can take the output from the builder, specifically the npm React snippet, and follow the npm path below. Just replace the content of the `components/custom-editor.js` file. The snippet may contain client-side hooks, so do not forget about adding the `'use client'` directive in the case of the App Router. + +## Setting up the project + +This guide assumes you already have a Next project. To create such a project, you can use CLI like `create-next-app`. Refer to the [Next.js documentation](https://nextjs.org/docs/app/api-reference/create-next-app) to learn more. + +## Installing from npm + +First, install the CKEditor 5 packages: + +* `ckeditor5` – package with open-source plugins and features. +* `ckeditor5-premium-features` – package with premium plugins and features. + +Depending on your configuration and chosen plugins, you may need to install the first or both packages. + +```bash +npm install ckeditor5 ckeditor5-premium-features +``` + +Next.js is based on React, so install the [CKEditor 5 WYSIWYG editor component for React](https://www.npmjs.com/package/@ckeditor/ckeditor5-react), too: + +```bash +npm install @ckeditor/ckeditor5-react +``` + +Next, you will use the installed dependencies in a React component. Create a new component in the components directory, for example, `components/custom-editor.js`. Inside the component file, import all necessary dependencies. Then, create a functional component that returns the CKEditor 5 React component. The below example shows how to use the component with open-source and premium plugins. + +App Router, by default, uses server components. It means you need to mark a component as client-side explicitly. You can achieve that by using the `'use client'` directive at the top of a file, above your imports. You do not need the directive if you use the Pages Router. + +```jsx +// components/custom-editor.js +'use client' // only in App Router + +import { CKEditor } from '@ckeditor/ckeditor5-react'; +import { ClassicEditor, Bold, Essentials, Italic, Mention, Paragraph, Undo } from 'ckeditor5'; +import { SlashCommand } from 'ckeditor5-premium-features'; + +import 'ckeditor5/ckeditor5.css'; +import 'ckeditor5-premium-features/ckeditor5-premium-features.css'; + +function CustomEditor() { + return ( + ', + mention: { + // Mention configuration + }, + initialData: '

Hello from CKEditor 5 in React!

' + } } + /> + ); +} + +export default CustomEditor; +``` + +The `CustomEditor` component is ready to be used inside a page. The page's directory will differ depending on the chosen routing strategy. + +CKEditor 5 is a client-side text editor and relies on the browser APIs, so you need to disable server-side rendering for our custom component. You can lazily load the component using the `dynamic()` function built into Next.js. + +```jsx +// app/page.js (App Router) +// pages/index.js (Pages Router) + +import dynamic from 'next/dynamic'; + +const CustomEditor = dynamic( () => import( '@/components/custom-editor' ), { ssr: false } ); + +function Home() { + return ( + + ); +} + +export default Home; +``` + +You can run your project now. If you chose `create-next-app`, type `npm run dev` to see your application in the browser. + + +If you have trouble seeing the editor, remember that the Next.js project ships with CSS files that can interfere with the editor. You can remove them or add your styling. + + +Also, pay attention to the import path - this guide uses the [default import alias](https://nextjs.org/docs/app/building-your-application/configuring/absolute-imports-and-module-aliases) (@). If you did not configure it, change the path appropriately. diff --git a/docs/getting-started/integrations-cdn/react-multiroot.md b/docs/getting-started/integrations-cdn/react-multiroot.md new file mode 100644 index 00000000000..33a373dbde2 --- /dev/null +++ b/docs/getting-started/integrations-cdn/react-multiroot.md @@ -0,0 +1,143 @@ +--- +menu-title: Multi-root integration +meta-title: React rich text editor component | CKEditor 5 documentation +category: react-cdn +order: 20 +modified_at: 2024-04-25 +--- + +{@snippet installation/integrations/framework-integration} + +# React rich text multi-root editor hook + +

+ + npm version + +

+ +This page focuses on describing the usage of the multi-root editor in React applications. If you would like to use a different type of editor, you can find more information {@link getting-started/integrations/react in this guide}. + + + The multi-root editors in React are supported since version 6.2.0 of this package. + + Unlike the {@link getting-started/integrations/react default integration}, we prepared the multi-root editor integration based on the hooks and new React mechanisms. + + +## Quick start + +This guide assumes you already have a React project. If you want to create a new one, you can use the [Vite](https://vitejs.dev/guide/) CLI. It allows you to create and customize your project with templates. For example, you can set up your project with TypeScript support. + +Install the [CKEditor 5 WYSIWYG editor package for React](https://www.npmjs.com/package/@ckeditor/ckeditor5-react) and the {@link getting-started/setup/editor-types#multi-root-editor multi-root editor type}. + +```bash +npm install ckeditor5 @ckeditor/ckeditor5-react +``` + +Use the `useMultiRootEditor` hook inside your project: + +```tsx +// App.jsx / App.tsx + +import { MultiRootEditor, Bold, Essentials, Italic, Paragraph } from 'ckeditor5'; +import { useMultiRootEditor } from '@ckeditor/ckeditor5-react'; + +import 'ckeditor5/ckeditor5.css'; + +const App = () => { + const editorProps = { + editor: MultiRootEditor, + data: { + intro: '

React multi-root editor

', + content: '

Hello from CKEditor 5 multi-root!

' + }, + config: { + plugins: [ Essentials, Bold, Italic, Paragraph ], + toolbar: { + items: [ 'undo', 'redo', '|', 'bold', 'italic' ] + }, + } + }; + + const { + editor, + toolbarElement, + editableElements, + data, + setData, + attributes, + setAttributes + } = useMultiRootEditor( editorProps ); + + return ( +
+

Using CKEditor 5 multi-root editor in React

+ + { toolbarElement } + + { editableElements } +
+ ); +} + +export default App; +``` + +## Hook properties + +The `useMultiRootEditor` hook supports the following properties: + +* `editor: MultiRootEditor` (required) – The {@link module:editor-multi-root/multirooteditor~MultiRootEditor `MultiRootEditor`} constructor to use. +* `data: Object` – The initial data for the created editor. See the {@link getting-started/setup/getting-and-setting-data Getting and setting data} guide. +* `rootsAttributes: Object` – The initial roots attributes for the created editor. +* `config: Object` – The editor configuration. See the {@link getting-started/setup/configuration Configuration} guide. +* `disabled: Boolean` – The {@link module:editor-multi-root/multirooteditor~MultiRootEditor `MultiRootEditor`} is being switched to read-only mode if the property is set to `true`. +* `disableWatchdog: Boolean` – If set to `true`, {@link features/watchdog the watchdog feature} will be disabled. It is set to `false` by default. +* `watchdogConfig: WatchdogConfig` – {@link module:watchdog/watchdog~WatchdogConfig Configuration object} for the [watchdog feature](https://ckeditor.com/docs/ckeditor5/latest/features/watchdog.html). +* `isLayoutReady: Boolean` – A property that delays the editor creation when set to `false`. It starts the initialization of the multi-root editor when sets to `true`. Useful when the CKEditor 5 annotations or a presence list are used. +* `disableTwoWayDataBinding: Boolean` – Allows disabling the two-way data binding mechanism between the editor state and `data` object to improve editor efficiency. The default value is `false`. +* `onReady: Function` – It is called when the editor is ready with a {@link module:editor-multi-root/multirooteditor~MultiRootEditor `MultiRootEditor`} instance. This callback is also called after the reinitialization of the component if an error occurred. +* `onChange: Function` – It is called when the editor data has changed. See the {@link module:engine/model/document~Document#event:change:data `editor.model.document#change:data`} event. +* `onBlur: Function` – It is called when the editor was blurred. See the {@link module:engine/view/document~Document#event:blur `editor.editing.view.document#blur`} event. +* `onFocus: Function` – It is called when the editor was focused. See the {@link module:engine/view/document~Document#event:focus `editor.editing.view.document#focus`} event. +* `onError: Function` – It is called when the editor has crashed during the initialization or during the runtime. It receives two arguments: the error instance and the error details. + Error details is an object that contains two properties: + * `phase: 'initialization'|'runtime'` – Informs when an error has occurred (during the editor or context initialization, or after the initialization). + * `willEditorRestart: Boolean` – If set to `true`, the editor component will restart itself. + +The editor event callbacks (`onChange`, `onBlur`, `onFocus`) receive two arguments: + +1. An {@link module:utils/eventinfo~EventInfo `EventInfo`} object. +2. An {@link module:editor-multi-root/multirooteditor~MultiRootEditor `MultiRootEditor`} instance. + +## Hook values + +The `useMultiRootEditor` hook returns the following values: + +* `editor` – The instance of created editor. +* `toolbarElement` – `ReactElement` that contains the toolbar. It could be rendered anywhere in the application. +* `editableElements` – An array of `ReactElements` that describes the editor's roots. This array is updated after detaching an existing root or adding a new root. +* `data` – The current state of the editor's data. It is updated after each editor update. Note that you should not use it if you disabled two-way binding by passing the `disableTwoWayDataBinding` property. +* `setData` – The function used for updating the editor's data. +* `attributes` – The current state of the editor's attributes. It is updated after each editor attributes update. Note that you should not use it if you disabled two-way binding by passing the `disableTwoWayDataBinding` property. +* `setAttributes` – The function used for updating the editor's attributes. + +## Context feature + +The `useMultiRootEditor` hook also supports the {@link features/context-and-collaboration-features context feature}, as described in the main {@link getting-started/integrations/react#context-feature React integration} guide. + +However, as the multi-root editor addresses most use cases of the context feature, consider if you need to employ it. + +## Two-way data binding + +By default, the two-way data binding is enabled. It means that every change done in the editor is automatically applied in the `data` object returned from the `useMultiRootEditor` hook. Additionally, if you want to change or set data in the editor, you can simply use `setData` method provided by the hook. It works the same way in case of attributes – the hook provides the `attributes` object and the `setAttributes` method to update them. It ensures that if you want to use or save the state of the editor, these objects are always up-to-date. + + + Two-way data binding may lead to performance issues with large editor content. In such cases, it is recommended to disable it by setting the `disableTwoWayDataBinding` property to `true` when using the `useMultiRootEditor` hook. When this is disabled, you will need to handle data synchronization manually if it is needed. + + The recommended approach for achieving this is based on utilizing the {@link features/autosave autosave plugin}. The second approach involves providing the `onChange` callback, which is called on each editor update. + + +## Contributing and reporting issues + +The source code of rich text editor component for React is available on GitHub in [https://github.com/ckeditor/ckeditor5-react](https://github.com/ckeditor/ckeditor5-react). diff --git a/docs/getting-started/integrations-cdn/react.md b/docs/getting-started/integrations-cdn/react.md new file mode 100644 index 00000000000..aeeeb1684bc --- /dev/null +++ b/docs/getting-started/integrations-cdn/react.md @@ -0,0 +1,284 @@ +--- +menu-title: Default integration +meta-title: React rich text editor component | CKEditor 5 documentation +category: react-cdn +order: 10 +--- + +{@snippet installation/integrations/framework-integration} + +# React rich text editor component + +

+ + npm version + +

+ +React lets you build user interfaces out of individual pieces called components. CKEditor 5 can be used as one of such components. + + + Starting from version 6.0.0 of this package, you can use native type definitions provided by CKEditor 5. Check the details about {@link getting-started/setup/typescript-support TypeScript support}. + + +## Quick start + +### Using CKEditor 5 Builder + +The easiest way to use CKEditor 5 in your React application is by configuring it with [CKEditor 5 Builder](https://ckeditor.com/builder?redirect=docs) and integrating it with your application. Builder offers an easy-to-use user interface to help you configure, preview, and download the editor suited to your needs. You can easily select: + +* the features you need, +* the preferred framework (React, Angular, Vue or Vanilla JS), +* the preferred distribution method. + +You get ready-to-use code tailored to your needs! + +### Setting up the project + +This guide assumes you have a React project. You can create a basic React project using [Vite](https://vitejs.dev/). Refer to the [React documentation](https://react.dev/learn/start-a-new-react-project) to learn how to set up a project in the framework. + +### Installing from npm + +First, install the CKEditor 5 packages: + +* `ckeditor5` – package with open-source plugins and features. +* `ckeditor5-premium-features` – package with premium plugins and features. + +Depending on your configuration and chosen plugins, you may need to install the first or both packages. + +```bash +npm install ckeditor5 ckeditor5-premium-features +``` + +Then, install the [CKEditor 5 WYSIWYG editor component for React](https://www.npmjs.com/package/@ckeditor/ckeditor5-react): + +```bash +npm install @ckeditor/ckeditor5-react +``` + +Use the `` component inside your project. The below example shows how to use the component with open-source and premium plugins. + +```jsx +import { CKEditor } from '@ckeditor/ckeditor5-react'; +import { ClassicEditor, Bold, Essentials, Italic, Mention, Paragraph, Undo } from 'ckeditor5'; +import { SlashCommand } from 'ckeditor5-premium-features'; + +import 'ckeditor5/ckeditor5.css'; +import 'ckeditor5-premium-features/ckeditor5-premium-features.css'; + +function App() { + return ( + ', + mention: { + // Mention configuration + }, + initialData: '

Hello from CKEditor 5 in React!

', + } } + /> + ); +} + +export default App; +``` + +Remember to import the necessary style sheets. The `ckeditor5` package contains the styles for open-source features, while the `ckeditor5-premium-features` package contains the premium features styles. + +## Component properties + +The `` component supports the following properties: + +* `editor` (required) – The {@link module:core/editor/editor~Editor `Editor`} constructor to use. +* `data` – The initial data for the created editor. See the {@link getting-started/setup/getting-and-setting-data Getting and setting data} guide. +* `config` – The editor configuration. See the {@link getting-started/setup/configuration Configuration} guide. +* `id` – The editor ID. When this property changes, the component restarts the editor with new data instead of setting it on an initialized editor. +* `disabled` – A Boolean value. The {@link module:core/editor/editor~Editor `editor`} is being switched to read-only mode if the property is set to `true`. +* `disableWatchdog` – A Boolean value. If set to `true`, {@link features/watchdog the watchdog feature} will be disabled. It is set to `false` by default. +* `watchdogConfig` – {@link module:watchdog/watchdog~WatchdogConfig Configuration object} for the [watchdog feature](https://ckeditor.com/docs/ckeditor5/latest/features/watchdog.html). +* `onReady` – A function called when the editor is ready with an {@link module:core/editor/editor~Editor `editor`} instance. This callback is also called after the reinitialization of the component if an error occurred. +* `onAfterDestroy` – A function called after the successful destruction of an editor instance rendered by the component. This callback is also triggered after the editor has been reinitialized after an error. The component is not guaranteed to be mounted when this function is called. +* `onChange` – A function called when the editor data has changed. See the {@link module:engine/model/document~Document#event:change:data `editor.model.document#change:data`} event. +* `onBlur` – A function called when the editor was blurred. See the {@link module:engine/view/document~Document#event:blur `editor.editing.view.document#blur`} event. +* `onFocus` – A function called when the editor was focused. See the {@link module:engine/view/document~Document#event:focus `editor.editing.view.document#focus`} event. +* `onError` – A function called when the editor has crashed during the initialization or during the runtime. It receives two arguments: the error instance and the error details. Error details is an object that contains two properties: + * `{String} phase`: `'initialization'|'runtime'` – Informs when the error has occurred (during the editor or context initialization, or after the initialization). + * `{Boolean} willEditorRestart` – When `true`, it means that the editor component will restart itself. + +The editor event callbacks (`onChange`, `onBlur`, `onFocus`) receive two arguments: + +1. An {@link module:utils/eventinfo~EventInfo `EventInfo`} object. +2. An {@link module:core/editor/editor~Editor `Editor`} instance. + +## Context feature + +The [`@ckeditor/ckeditor5-react`](https://www.npmjs.com/package/@ckeditor/ckeditor5-react) package provides a ready-to-use component for the {@link features/context-and-collaboration-features context feature} that is useful when used together with some {@link features/collaboration CKEditor 5 collaboration features}. + +```jsx +import { ClassicEditor, Context, Bold, Essentials, Italic, Paragraph, ContextWatchdog } from 'ckeditor5'; +import { CKEditor, CKEditorContext } from '@ckeditor/ckeditor5-react'; + +import 'ckeditor5/ckeditor5.css'; + +function App() { + return ( + + { + // You can store the "editor" and use when it is needed. + console.log( 'Editor 1 is ready to use!', editor ); + } } + /> + + { + // You can store the "editor" and use when it is needed. + console.log( 'Editor 2 is ready to use!', editor ); + } } + /> + + ); +} + +export default App; +``` + +The `CKEditorContext` component supports the following properties: + +* `context` (required) – {@link module:core/context~Context The CKEditor 5 context class}. +* `contextWatchdog` (required) – {@link module:watchdog/contextwatchdog~ContextWatchdog The Watchdog context class}. +* `config` – The CKEditor 5 context configuration. +* `isLayoutReady` – A property that delays the context creation when set to `false`. It creates the context and the editor children once it is `true` or unset. Useful when the CKEditor 5 annotations or a presence list are used. +* `id` – The context ID. When this property changes, the component restarts the context with its editor and reinitializes it based on the current configuration. +* `onReady` – A function called when the context is ready and all editors inside were initialized with the `context` instance. This callback is also called after the reinitialization of the component if an error has occurred. +* `onError` – A function called when the context has crashed during the initialization or during the runtime. It receives two arguments: the error instance and the error details. Error details is an object that contains two properties: + * `{String} phase`: `'initialization'|'runtime'` – Informs when the error has occurred (during the editor or context initialization, or after the initialization). + * `{Boolean} willContextRestart` – When `true`, it means that the context component will restart itself. + + + An example build that exposes both context and classic editor can be found in the [CKEditor 5 collaboration sample](https://github.com/ckeditor/ckeditor5-collaboration-samples/blob/master/real-time-collaboration-comments-outside-of-editor-for-react). + + +## How to? + +### Using the document editor type + +If you use the {@link framework/document-editor document (decoupled) editor}, you need to {@link module:editor-decoupled/decouplededitor~DecoupledEditor.create add the toolbar to the DOM manually}: + +```jsx +import { useEffect, useRef, useState } from 'react'; +import { DecoupledEditor, Bold, Essentials, Italic, Paragraph } from 'ckeditor5'; +import { CKEditor } from '@ckeditor/ckeditor5-react'; + +import 'ckeditor5/ckeditor5.css'; + +function App() { + const editorToolbarRef = useRef( null ); + const [ isMounted, setMounted ] = useState( false ); + + useEffect( () => { + setMounted( true ); + + return () => { + setMounted( false ); + }; + }, [] ); + + return ( +
+
+
+ { isMounted && ( + { + if ( editorToolbarRef.current ) { + editorToolbarRef.current.appendChild( editor.ui.view.toolbar.element ); + } + }} + onAfterDestroy={ ( editor ) => { + if ( editorToolbarRef.current ) { + Array.from( editorToolbarRef.current.children ).forEach( child => child.remove() ); + } + }} + /> + ) } +
+
+ ); +} + +export default App; +``` + +### Using the editor with collaboration plugins + +We provide a few **ready-to-use integrations** featuring collaborative editing in React applications: + +* [CKEditor 5 with real-time collaboration features](https://github.com/ckeditor/ckeditor5-collaboration-samples/tree/master/real-time-collaboration-for-react) +* [CKEditor 5 with real-time collaboration and revision history features](https://github.com/ckeditor/ckeditor5-collaboration-samples/tree/master/real-time-collaboration-revision-history-for-react) +* [CKEditor 5 with the revision history feature](https://github.com/ckeditor/ckeditor5-collaboration-samples/tree/master/revision-history-for-react) +* [CKEditor 5 with the track changes feature](https://github.com/ckeditor/ckeditor5-collaboration-samples/tree/master/track-changes-for-react) + +It is not mandatory to build applications on top of the above samples, however, they should help you get started. + +### Localization + +CKEditor 5 supports {@link getting-started/setup/ui-language multiple UI languages}, and so does the official React component. Follow the instructions below to translate CKEditor 5 in your React application. + +Similarly to CSS style sheets, both packages have separate translations. Import them as shown in the example below. Then, pass them to the `translations` array inside the `config` prop in the CKEditor 5 component. + +```jsx +import { ClassicEditor } from 'ckeditor5'; +import { CKEditor } from '@ckeditor/ckeditor5-react'; +// More imports... + +import coreTranslations from 'ckeditor5/translations/es.js'; +import premiumFeaturesTranslations from 'ckeditor5-premium-features/translations/es.js'; + +// Style sheets imports... + +function App() { + return ( + Hola desde CKEditor 5 en React!

', + } } + /> + ); +} + +export default App; +``` + +For more information, please refer to the {@link getting-started/setup/ui-language Setting the UI language} guide. + +## Contributing and reporting issues + +The source code of rich text editor component for React is available on GitHub in [https://github.com/ckeditor/ckeditor5-react](https://github.com/ckeditor/ckeditor5-react). diff --git a/docs/getting-started/integrations-cdn/vuejs-v2.md b/docs/getting-started/integrations-cdn/vuejs-v2.md new file mode 100644 index 00000000000..934e698c259 --- /dev/null +++ b/docs/getting-started/integrations-cdn/vuejs-v2.md @@ -0,0 +1,452 @@ +--- +menu-title: Vue.js 2.x +meta-title: Vue.js 2.x rich text editor component | CKEditor 5 documentation +category: cloud +order: 40 +--- + +{@snippet installation/integrations/framework-integration} + +# Vue.js 2.x rich text editor component + +

+ + npm version + +

+ + + This guide is about the CKEditor 5 integration with Vue.js 2.x. However, Vue 2 has reached EOL and is no longer actively maintained. To learn more about the integration with Vue.js 3+, check out the {@link getting-started/integrations/vuejs-v3 "Rich text editor component for Vue.js 3+"} guide. + + +Vue.js is a versatile framework for building web user interfaces. CKEditor 5 provides the official Vue component you can use in your application. + + + The {@link features/watchdog watchdog feature} is available for the {@link getting-started/integrations/react React} and {@link getting-started/integrations/angular Angular} integrations, but is not supported in Vue yet. + + +## Quick start + +### Using CKEditor 5 Builder + +The easiest way to use CKEditor 5 in your Vue application is by configuring it with [CKEditor 5 Builder](https://ckeditor.com/builder?redirect=docs) and integrating it with your application. Builder offers an easy-to-use user interface to help you configure, preview, and download the editor suited to your needs. You can easily select: + +* the features you need, +* the preferred framework (React, Angular, Vue or Vanilla JS), +* the preferred distribution method. + +You get ready-to-use code tailored to your needs! + +### Installing from npm + +This guide assumes you already have a Vue project. First, install the CKEditor 5 packages: + +* `ckeditor5` – package with open-source plugins and features. +* `ckeditor5-premium-features` – package with premium plugins and features. + +```bash +npm install ckeditor5 ckeditor5-premium-features +``` + +Depending on your configuration and chosen plugins, you may need to install the first or both packages. + +Then, install the [CKEditor 5 WYSIWYG editor component for Vue 2](https://www.npmjs.com/package/@ckeditor/ckeditor5-vue2): + +```bash +npm install @ckeditor/ckeditor5-vue2 +``` + +To create an editor instance, you must first import the editor and the component modules into the root file of your application (for example, `main.js` when generated by `create-vue`). + +```js +import Vue from 'vue'; +import CKEditor from '@ckeditor/ckeditor5-vue2'; +import App from './App.vue'; + +Vue.use( CKEditor ); + +new Vue( { render: ( h ) => h( App ) } ).$mount( '#app' ); +``` + +Use the `` component inside the template tag. The below example shows how to use the component with open-source and premium plugins. + +```html + + + +``` + +### Using the component locally + +If you do not want the CKEditor component to be enabled globally, you can skip the `Vue.use( CKEditor )` part entirely. Instead, configure it in the `components` property of your view. + +```html + + + +``` + +## Component directives + +### `editor` + +This directive specifies the editor to be used by the component. It must directly reference the editor constructor to be used in the template. + +```html + + + +``` + +### `tag-name` + +By default, the editor component creates a `
` container which is used as an element passed to the editor (for example, {@link module:editor-classic/classiceditorui~ClassicEditorUI#element `ClassicEditor#element`}). The element can be configured, so for example to create a ` + + + +``` + +### Step 2: Replace CKEditor 5 imports with `window.CKEDITOR` + +Since the CKEditor 5 script is now included via the CDN, you can access the `ClassicEditor` object directly in your JavaScript file using the `window.CKEDITOR` global variable. It means that `import` statements are no longer needed and you can remove them from your JavaScript files. Here is an example of migrating the CKEditor 5 initialization code: + +**Before:** + +```javascript +import { ClassicEditor } from 'ckeditor5'; +import { AIAdapter, /* ... other imports */ } from 'ckeditor5-premium-features'; + +ClassicEditor + .create( document.querySelector('#editor'), { + licenseKey: '', // Or 'GPL'. + // ... other configuration + } ) + .catch( error => { + console.error(error); + } ); +``` + +**After:** + +```javascript +const { ClassicEditor } = window.CKEDITOR; +const { AIAdapter, /* ... other imports */ } = window.CKEDITOR_PREMIUM_FEATURES; + +ClassicEditor + .create( document.querySelector('#editor'), { + licenseKey: '', // Or 'GPL'. + // ... other configuration + } ) + .catch( error => { + console.error(error); + } ); +``` + +## Using lazy injection of CKEditor 5 + +If you prefer to automatically inject the CKEditor 5 script into your HTML file, you can migrate your project using the `@ckeditor/ckeditor5-integrations-common` package. This package provides a `loadCKEditorCloud` function that automatically injects the CKEditor 5 scripts and styles into your HTML file. It may be useful when your project uses a bundler like Webpack or Rollup and you cannot modify your head section directly. + +### Step 1: Install the `@ckeditor/ckeditor5-integrations-common` Package + +First, install the `@ckeditor/ckeditor5-integrations-common` package using the following command: + +```bash +npm install @ckeditor/ckeditor5-integrations-common +``` + +### Step 2: Replace CKEditor 5 Imports + +If you have any CKEditor 5 imports in your JavaScript files, remove them. For example, remove lines like: + +```javascript +import { ClassicEditor, ... } from 'ckeditor5'; +import { AIAdapter, ... } from 'ckeditor5-premium-features'; +``` + +Next, update your JavaScript file to use the `loadCKEditorCloud` function from the `@ckeditor/ckeditor5-integrations-common` package. Here is an example of migrating the CKEditor 5 initialization code: + +**Before:** + +```javascript +import { ClassicEditor } from 'ckeditor5'; + +ClassicEditor + .create( document.querySelector('#editor') ) + .catch( error => { + console.error(error); + } ); +``` + +**After:** + +```javascript +import { loadCKEditorCloud } from '@ckeditor/ckeditor5-integrations-common'; + +const { ClassicEditor } = await loadCKEditorCloud( { + version: '{@var ckeditor5-version}', +} ); +``` + +## Conclusion + +Following these steps, you successfully migrated CKEditor 5 from an NPM-based installation to a CDN-based installation using Vanilla JS. This approach simplifies the setup process and can help improve the performance of your application by reducing the bundle size. diff --git a/docs/updating/migration-to-cdn/vuejs-v3.md b/docs/updating/migration-to-cdn/vuejs-v3.md new file mode 100644 index 00000000000..889017ab451 --- /dev/null +++ b/docs/updating/migration-to-cdn/vuejs-v3.md @@ -0,0 +1,189 @@ +--- +menu-title: Vue 3+ +meta-title: Vue CKEditor 5 - migrate integration from npm to CDN | CKEditor 5 documentation +meta-description: Migrate Vue 3+ CKEditor 5 integration from npm to CDN in a few simple steps. Learn how to install Vue 3+ CKEditor 5 integration in your project using the CDN. +category: migrations +order: 40 +--- + +# Migrating Vue 3+ CKEditor 5 integration from npm to CDN + +This guide will help you migrate Vue 3 CKEditor 5 integration from an NPM-based installation to a CDN-based one. + +## Prerequisites + +Remove the existing CKEditor 5 packages from your project. If you are using the NPM-based installation, you can remove it by running the following command: + +```bash +npm uninstall ckeditor5 ckeditor5-premium-features +``` + +Upgrade the CKEditor 5 Vue 3 integration to the latest version. You can find the latest version in the {@link getting-started/integrations-cdn/vuejs-v3 Vue 3 integration} documentation. + +Ensure that your testing suite uses real web browser environments for testing. If you are using `jsdom` or any other environment without a real DOM, you may need to adjust the testing suite configuration to use a real browser because CDN script injection might not be recognized properly in such environments. + +## Migration steps + +### Step 1: Remove CKEditor 5 imports + +If you have any CKEditor 5 imports in your Vue components, remove them. For example, remove lines like: + +```javascript +import { ClassicEditor, /* ... other imports */ } from 'ckeditor5'; +import { AIAdapter, /* ... other imports */ } from 'ckeditor5-premium-features'; +``` + +### Step 2: Update your Vue components to use CDN + +Replace the CKEditor 5 NPM package imports with the CDN script imports and use the `useCKEditorCloud` function to load the CKEditor 5 scripts. The `useCKEditorCloud` function is a part of the `@ckeditor/ckeditor5-vue` package and is used to load CKEditor 5 scripts from the CKEditor Cloud service. + +**Before:** + +```html + + + +``` + +**After:** + +```html + + + +``` + +### Step 3 (Optional): Migrate the CKEditor 5 Vue 3+ integration testing suite + +If you have any tests that use CKEditor 5 objects, you need to update them to use the `loadCKEditorCloud` function. Here is an example of migrating a test that uses the `ClassicEditor` object: + +**Before:** + +```javascript +import { ClassicEditor, /* ... other imports */ } from 'ckeditor5'; + +it( 'ClassicEditor test', () => { + // Your test that uses the CKEditor 5 object. +} ); +``` + +**After:** + +```javascript +// It may be counterintuitive that in tests you need to use `loadCKEditorCloud` instead of `useCKEditorCloud`. +// The reason for this is that `useCKEditorCloud` is composable and can only be used in Vue components, +// while tests are typically written as functions in testing suites. Therefore, in tests, you should use +// the `loadCKEditorCloud` function to load CKEditor 5 from the CKEditor Cloud and obtain the necessary +// CKEditor 5 objects. This allows you to properly test your CKEditor 5 integration without any issues. + +import { loadCKEditorCloud } from '@ckeditor/ckeditor5-vue'; + +let cloud; + +beforeEach( async () => { + cloud = await loadCKEditorCloud( { + version: '{@var ckeditor5-version}', + } ); +} ); + +it( 'ClassicEditor test', () => { + const { ClassicEditor, ... } = cloud.CKEditor; + + // Your test that uses the CKEditor 5 object. +} ); +``` + +#### Step 4 (Optional): Clean up the document head entries before each test + +The `useCKEditorCloud` composable under the hood injects the CKEditor 5 scripts and styles into your document head. If you use a testing suite that does not Clean up the document head entries before each test, you may need to do it manually. This is important because the `useCKEditorCloud` composable might reuse the same head entries for each test, which can lead to skipping the `loading` state and directly going to the `success` state. It may cause some tests that rely on the `loading` state to fail. + +However, there is one downside to this approach. Cleaning up the head entries before each test may slow down the test execution because the browser needs to download the CKEditor 5 script each time. In most cases, this should not be a problem, but if you notice that your tests are running slower, you may need to consider other solutions. + +Here is an example of how you can Clean up the document head entries before each test: + +```javascript +import { removeAllCkCdnResources } from '@ckeditor/ckeditor5-integrations-common/test-utils'; + +beforeEach( () => { + removeAllCkCdnResources(); +} ); +``` + +The code above will remove all CKEditor 5 CDN scripts, style sheets, and Window objects from the head section of your HTML file before each test, making sure that the `useCKEditorCloud` composable will inject the CKEditor 5 scripts and styles again. From f9a3dc7f3ec8c34a47963abed1f16ced4a46741b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gorzeli=C5=84ski?= Date: Mon, 9 Sep 2024 14:24:30 +0200 Subject: [PATCH 186/256] Docs: Post-review fixes in the React CDN guide. --- .../integrations-cdn/react-default-cdn.md | 94 +++++++++---------- 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/docs/getting-started/integrations-cdn/react-default-cdn.md b/docs/getting-started/integrations-cdn/react-default-cdn.md index ed9752ba77c..99b0d454392 100644 --- a/docs/getting-started/integrations-cdn/react-default-cdn.md +++ b/docs/getting-started/integrations-cdn/react-default-cdn.md @@ -8,7 +8,7 @@ order: 10 {@snippet installation/integrations/framework-integration} -# React rich text editor component in CDN +# React rich text editor component (CDN)

@@ -16,7 +16,7 @@ order: 10

-This guide explains how to integrate CKEditor 5 into your React application using CDN. +React lets you build user interfaces out of individual pieces called components. CKEditor 5 can be used as one of such components. This guide explains how to integrate CKEditor 5 into your React application using CDN. Starting from version 6.0.0 of this package, you can use native type definitions provided by CKEditor 5. Check the details about {@link getting-started/setup/typescript-support TypeScript support}. @@ -48,11 +48,11 @@ npm install @ckeditor/ckeditor5-react The `useCKEditorCloud` hook is responsible for returning information that: -* CKEditor is still being downloaded from the CDN with status = "loading". -* An error occurred during the download when status = "error", then further information is returned in the error field. -* Returning the editor in the data field and its dependencies when status = "success". +* The editor is still downloading from the CDN with the `status = "loading"`. +* An error occurred during the download when `status = "error"`. Further information is in the error field. +* About the editor in the data field and its dependencies when `status = "success"`. -Use the `` component inside your project. The below example shows how to use the component with the open-source plugins. +Use the `` component inside your project. The below example shows how to use it with the open-source plugins. ```js import React from "react"; @@ -68,7 +68,7 @@ const CKEditorDemo = () => { } if ( cloud.status === "loading" ) { - eturn
Loading...
; + return
Loading...
; } const { @@ -82,14 +82,14 @@ const CKEditorDemo = () => { return ( Hello world!!!

"} - config={{ + editor={ ClassicEditor } + data={ "

Hello world!!!

" } + config={ { toolbar: { - items: ["undo", "redo", "|", "bold", "italic"], + items: [ "undo", "redo", "|", "bold", "italic" ], }, plugins: [ Bold, Essentials, Italic, Paragraph, Undo ], - }} + } } /> ); }; @@ -102,10 +102,10 @@ import React from "react"; import { CKEditor, useCKEditorCloud } from "@ckeditor/ckeditor5-react"; const CKEditorDemo = () => { - const cloud = useCKEditorCloud({ + const cloud = useCKEditorCloud( { version: "{@var ckeditor5-version}", premium: true, - }); + } ); if ( cloud.status === "error" ) { return
Error!
; @@ -131,7 +131,7 @@ const CKEditorDemo = () => { Hello world!!!

" } - config={{ + config={ { licenseKey: "", toolbar: { items: [ "undo", "redo", "|", "bold", "italic" ], @@ -145,7 +145,7 @@ const CKEditorDemo = () => { Undo, SlashCommand, ], - }} + } } /> ); }; @@ -160,20 +160,20 @@ import React from "react"; import { CKEditor, useCKEditorCloud } from "@ckeditor/ckeditor5-react"; const CKEditorDemo = () => { - const cloud = useCKEditorCloud({ - version: "{@var ckeditor5-version}", - ckbox: { - version: "2.5.1", - // Optional - it's already 'lark' by default. - theme: "lark", - }, - }); + const cloud = useCKEditorCloud( { + version: "{@var ckeditor5-version}", + ckbox: { + version: "2.5.1", + // Optional - it's already 'lark' by default. + theme: "lark", + }, + } ); - if (cloud.status === "error") { + if ( cloud.status === "error" ) { return
Error!
; } - if (cloud.status === "loading") { + if ( cloud.status === "loading" ) { return
Loading...
; } @@ -191,11 +191,11 @@ const CKEditorDemo = () => { return ( Hello world!!!

"} - config={{ + editor={ ClassicEditor } + data={ "

Hello world!!!

" } + config={ { toolbar: { - items: ["undo", "redo", "|", "bold", "italic"], + items: [ "undo", "redo", "|", "bold", "italic" ], }, plugins: [ Bold, @@ -210,9 +210,9 @@ const CKEditorDemo = () => { ckbox: { tokenUrl: "https://api.ckbox.io/token/demo", forceDemoLabel: true, - allowExternalImagesEditing: [/^data:/, /^i.imgur.com\//, "origin"], + allowExternalImagesEditing: [ /^data:/, /^i.imgur.com\//, "origin" ], }, - }} + } } /> ); }; @@ -222,10 +222,10 @@ const CKEditorDemo = () => { There are various ways to use external plugins. Here is a list of them: -- **Local UMD Plugins:** Dynamically import local UMD modules using the `import()` syntax. -- **Local External Imports:** Load external plugins locally using additional bundler configurations (such as Vite). -- **CDN 3rd Party Plugins:** Load JavaScript and CSS files from a CDN by specifying the URLs. -- **Verbose Configuration:** Advanced plugin loading with options to specify both script and style sheet URLs, along with an optional `checkPluginLoaded` function to verify the plugin has been correctly loaded into the global scope. +* **Local UMD Plugins:** Dynamically import local UMD modules using the `import()` syntax. +* **Local External Imports:** Load external plugins locally using additional bundler configurations (such as Vite). +* **CDN 3rd Party Plugins:** Load JavaScript and CSS files from a CDN by specifying the URLs. +* **Verbose Configuration:** Advanced plugin loading with options to specify both script and style sheet URLs, along with an optional `checkPluginLoaded` function to verify the plugin has been correctly loaded into the global scope. Here is an example: @@ -240,8 +240,8 @@ const CKEditorDemo = () => { PluginUMD: async () => import( './your-local-import.umd.js' ), PluginLocalImport: async () => import( './your-local-import' ), PluginThirdParty: [ - 'https://cdn.example.com/plugin2.js', - 'https://cdn.example.com/plugin2.css' + 'https://cdn.example.com/plugin3.js', + 'https://cdn.example.com/plugin3.css' ] } } ); @@ -254,7 +254,7 @@ const CKEditorDemo = () => { return
Loading...
; } - const { Plugin1, Plugin2, /* More plugins... */ } = cloud.loadedPlugins; + const { PluginUMD, PluginLocalImport, PluginThirdParty } = cloud.loadedPlugins; // ... }; ``` @@ -276,8 +276,8 @@ The `` component supports the following properties: * `onBlur` – A function called when the editor was blurred. See the {@link module:engine/view/document~Document#event:blur `editor.editing.view.document#blur`} event. * `onFocus` – A function called when the editor was focused. See the {@link module:engine/view/document~Document#event:focus `editor.editing.view.document#focus`} event. * `onError` – A function called when the editor has crashed during the initialization or during the runtime. It receives two arguments: the error instance and the error details. Error details is an object that contains two properties: -* `phase`: `'initialization'|'runtime'` – Informs when the error has occurred (during the editor or context initialization, or after the initialization). -* `willEditorRestart` – When `true`, it means that the editor component will restart itself. +* `phase`: `'initialization'|'runtime'` – Informs when the error has occurred (during or after the editor/context initialization). +* `willEditorRestart` – When `true`, it means the editor component will restart itself. The editor event callbacks (`onChange`, `onBlur`, `onFocus`) receive two arguments: @@ -294,7 +294,7 @@ The `` component supports the following properties: ## Context feature -The [`@ckeditor/ckeditor5-react`](https://www.npmjs.com/package/@ckeditor/ckeditor5-react) package provides a ready-to-use component for the {@link features/context-and-collaboration-features context feature} that is useful when used together with some {@link features/collaboration CKEditor 5 collaboration features}. +The [`@ckeditor/ckeditor5-react`](https://www.npmjs.com/package/@ckeditor/ckeditor5-react) package provides a ready-to-use component for the {@link features/context-and-collaboration-features context feature} that is useful to use with some {@link features/collaboration CKEditor 5 collaboration features}. ```jsx import React from 'react'; @@ -357,12 +357,12 @@ function CKEditorNestedInstanceDemo( { name, content } ) { return ( ); } @@ -435,7 +435,7 @@ The `CKEditorContext` component supports the following properties: * `onChangeInitializedEditors` – A function called when any editor is initialized or destroyed in the tree. It receives a dictionary of fully initialized editors, where the key is the value of the `contextItemMetadata.name` property set on the `CKEditor` component. The editor's ID is the key if the `contextItemMetadata` property is absent. Additional data can be added to the `contextItemMetadata` in the `CKEditor` component, which will be passed to the `onChangeInitializedEditors` function. * `onReady` – A function called when the context is ready and all editors inside were initialized with the `context` instance. This callback is also called after the reinitialization of the component if an error has occurred. * `onError` – A function called when the context has crashed during the initialization or during the runtime. It receives two arguments: the error instance and the error details. Error details is an object that contains two properties: -* `phase`: `'initialization'|'runtime'` – Informs when the error has occurred (during the editor or context initialization, or after the initialization). +* `phase`: `'initialization'|'runtime'` – Informs when the error has occurred (during or after the editor/context initialization). * `willContextRestart` – When `true`, it means that the context component will restart itself. From f0d48c26d04d35925ee384292b2af9446e3907a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gorzeli=C5=84ski?= Date: Mon, 9 Sep 2024 14:40:23 +0200 Subject: [PATCH 187/256] Docs: Post-review fixes in the React multiroot CDN guide. --- .../integrations-cdn/react-multiroot-cdn.md | 222 +++++++++--------- 1 file changed, 111 insertions(+), 111 deletions(-) diff --git a/docs/getting-started/integrations-cdn/react-multiroot-cdn.md b/docs/getting-started/integrations-cdn/react-multiroot-cdn.md index f95ef8c3997..999f3b417c1 100644 --- a/docs/getting-started/integrations-cdn/react-multiroot-cdn.md +++ b/docs/getting-started/integrations-cdn/react-multiroot-cdn.md @@ -41,126 +41,126 @@ Use the `useMultiRootEditor` hook inside your project: import React from "react"; import { useMultiRootEditor, withCKCloud } from "@ckeditor/ckeditor5-react"; import { - useMultiRootEditor, - withCKEditorCloud, - type MultiRootHookProps, - type WithCKEditorCloudHocProps, + useMultiRootEditor, + withCKEditorCloud, + type MultiRootHookProps, + type WithCKEditorCloudHocProps, } from "@ckeditor/ckeditor5-react"; type EditorDemoProps = WithCKEditorCloudHocProps & { - data: Record; + data: Record; }; -const withCKCloud = withCKEditorCloud({ - cloud: { - version: "{@var ckeditor5-version}", - languages: ["de"], - premium: true, - }, +const withCKCloud = withCKEditorCloud( { + cloud: { + version: "{@var ckeditor5-version}", + languages: [ "de" ], + premium: true, + }, - // Optional: - renderError: (error) =>
Error!
, + // Optional: + renderError: ( error ) =>
Error!
, - // Optional: - renderLoader: () =>
Loading...
, -}); + // Optional: + renderLoader: () =>
Loading...
, +} ); const MultiRootEditorDemo = withCKCloud( - ({ data, cloud }: EditorDemoProps): ReactNode => { - const { - MultiRootEditor: MultiRootEditorBase, - CloudServices, - Essentials, - CKFinderUploadAdapter, - Autoformat, - Bold, - Italic, - BlockQuote, - CKBox, - CKFinder, - EasyImage, - Heading, - Image, - ImageCaption, - ImageStyle, - ImageToolbar, - ImageUpload, - Indent, - Link, - List, - MediaEmbed, - Paragraph, - PasteFromOffice, - PictureEditing, - Table, - TableToolbar, - TextTransformation, - } = cloud.CKEditor; - - class MultiRootEditor extends MultiRootEditorBase { - public static override builtinPlugins = [ - Essentials, - CKFinderUploadAdapter, - Autoformat, - Bold, - Italic, - BlockQuote, - CKBox, - CKFinder, - CloudServices, - EasyImage, - Heading, - Image, - ImageCaption, - ImageStyle, - ImageToolbar, - ImageUpload, - Indent, - Link, - List, - MediaEmbed, - Paragraph, - PasteFromOffice, - PictureEditing, - TextTransformation, - ]; - - public static override defaultConfig = { - toolbar: { - items: [ - "undo", - "redo", - "|", - "heading", - "|", - "bold", - "italic", - "|", - "link", - "uploadImage", - "blockQuote", - "mediaEmbed", - "|", - "bulletedList", - "numberedList", - "outdent", - "indent", - ], - }, - }; - } - - const { toolbarElement, editableElements } = useMultiRootEditor({ - editor: MultiRootEditor, - data, - }); - - return ( -
- {toolbarElement} - {editableElements} -
- ); + ( { data, cloud }: EditorDemoProps ): ReactNode => { + const { + MultiRootEditor: MultiRootEditorBase, + CloudServices, + Essentials, + CKFinderUploadAdapter, + Autoformat, + Bold, + Italic, + BlockQuote, + CKBox, + CKFinder, + EasyImage, + Heading, + Image, + ImageCaption, + ImageStyle, + ImageToolbar, + ImageUpload, + Indent, + Link, + List, + MediaEmbed, + Paragraph, + PasteFromOffice, + PictureEditing, + Table, + TableToolbar, + TextTransformation, + } = cloud.CKEditor; + + class MultiRootEditor extends MultiRootEditorBase { + public static override builtinPlugins = [ + Essentials, + CKFinderUploadAdapter, + Autoformat, + Bold, + Italic, + BlockQuote, + CKBox, + CKFinder, + CloudServices, + EasyImage, + Heading, + Image, + ImageCaption, + ImageStyle, + ImageToolbar, + ImageUpload, + Indent, + Link, + List, + MediaEmbed, + Paragraph, + PasteFromOffice, + PictureEditing, + TextTransformation, + ]; + + public static override defaultConfig = { + toolbar: { + items: [ + "undo", + "redo", + "|", + "heading", + "|", + "bold", + "italic", + "|", + "link", + "uploadImage", + "blockQuote", + "mediaEmbed", + "|", + "bulletedList", + "numberedList", + "outdent", + "indent", + ], + }, + }; + } + + const { toolbarElement, editableElements } = useMultiRootEditor( { + editor: MultiRootEditor, + data, + } ); + + return ( +
+ { toolbarElement } + { editableElements } +
+ ); } ); ``` From 526c42c94021d53949103fd199653ac56e414539 Mon Sep 17 00:00:00 2001 From: Filip Sobol Date: Mon, 9 Sep 2024 15:09:36 +0200 Subject: [PATCH 188/256] Docs: Update Vue docs for use with npm and CDN. --- .../integrations-cdn/vuejs-v3.md | 176 +++++++----------- docs/getting-started/integrations/vuejs-v3.md | 99 +++------- 2 files changed, 95 insertions(+), 180 deletions(-) diff --git a/docs/getting-started/integrations-cdn/vuejs-v3.md b/docs/getting-started/integrations-cdn/vuejs-v3.md index fc051eee10c..b918b73911d 100644 --- a/docs/getting-started/integrations-cdn/vuejs-v3.md +++ b/docs/getting-started/integrations-cdn/vuejs-v3.md @@ -8,7 +8,7 @@ order: 50 {@snippet installation/integrations/framework-integration} -# Vue.js 3+ rich text editor component +# Vue.js 3+ rich text editor component (CDN)

@@ -16,142 +16,106 @@ order: 50

-Vue.js is a versatile framework for building web user interfaces. CKEditor 5 provides the official Vue component you can use in your application. +CKEditor 5 has an official Vue integration that you can use to add a rich text editor to your application. This guide will help you install it and configure to use the CDN distribution of the CKEditor 5. - - Starting from version 5.0.0 of this package, you can use native type definitions provided by CKEditor 5. Check the details about {@link getting-started/setup/typescript-support TypeScript support}. - +This guide assumes that you already have a Vue project. If you do not have one, see the [Vue documentation](https://vuejs.org/guide/quick-start) to learn how to create it. ## Quick start {@snippet getting-started/use-builder} -### Setting up the project - -This guide assumes that you already have a Vue project. If you do not have one, see the [Vue documentation](https://vuejs.org/guide/quick-start) to learn how to create it. - -### Installation - -Start by installing the following packages: - -* `ckeditor5` – contains all open-source plugins and features for CKEditor 5. - - ```bash - npm install ckeditor5 - ``` - -* `ckeditor5-premium-features` – contains premium plugins and features for CKEditor 5. Depending on your configuration and chosen plugins, you might not need it. - - ```bash - npm install ckeditor5-premium-features - ``` - -* `@ckeditor/ckeditor5-vue` – the [CKEditor 5 WYSIWYG editor component for Vue](https://www.npmjs.com/package/@ckeditor/ckeditor5-vue). - - ```bash - npm install @ckeditor/ckeditor5-vue - ``` - -With these packages installed, you now need to choose whether to install the `` component globally or locally and follow the appropriate instructions below. - -#### Installing the `` component globally - -To register the `` component globally, you must install the CKEditor 5 plugin for Vue. - -If you are using a plain Vue project, you should find the file where the `createApp` function is called and register the `CkeditorPlugin` plugin with the [`use()` method](https://vuejs.org/api/application.html#app-use). +### Installing and configuring the Vue integration -```js -import { createApp } from 'vue'; -import { CkeditorPlugin } from '@ckeditor/ckeditor5-vue'; -import App from './App.vue'; +Start by installing the Vue integration for CKEditor 5 from npm: -createApp( App ) - .use( CkeditorPlugin ) - .mount( '#app' ); +```bash +npm install @ckeditor/ckeditor5-vue ``` -If you are using Nuxt.js, you can follow the [Nuxt.js documentation](https://nuxt.com/docs/guide/directory-structure/plugins#vue-plugins) to get access to the `use()` method and register this plugin. - -Now you can use the `` component in any of your Vue components. The following example shows a single file component with open source and premium plugins. +Once the integration is installed, create a new Vue component called `Editor.vue`. It will use the `useCKEditorCloud` helper to load the editor code from CDN and the `` component to run it. The following example shows a single file component with open source and premium CKEditor 5 plugins. ```html ``` -#### Using the `` component locally - -If you do not want to enable the CKEditor 5 component globally, you can import the `Ckeditor` component from the `@ckeditor/ckeditor5-vue` package directly into the Vue component where you want to use it, and add it to the `components` object. +Now you can import and use the `Editor.vue` component anywhere in your application. ```html +``` - +```html + ``` ## Component directives diff --git a/docs/getting-started/integrations/vuejs-v3.md b/docs/getting-started/integrations/vuejs-v3.md index bc1291e3804..5190aeb422d 100644 --- a/docs/getting-started/integrations/vuejs-v3.md +++ b/docs/getting-started/integrations/vuejs-v3.md @@ -16,20 +16,14 @@ order: 50

-Vue.js is a versatile framework for building web user interfaces. CKEditor 5 provides the official Vue component you can use in your application. +CKEditor 5 has an official Vue integration that you can use to add rich text editor to your application. This guide will help you install it and configure to use the npm distribution of the CKEditor 5. - - Starting from version 5.0.0 of this package, you can use native type definitions provided by CKEditor 5. Check the details about {@link getting-started/setup/typescript-support TypeScript support}. - +This guide assumes that you already have a Vue project. If you do not have one, see the [Vue documentation](https://vuejs.org/guide/quick-start) to learn how to create it. ## Quick start {@snippet getting-started/use-builder} -### Setting up the project - -This guide assumes that you already have a Vue project. If you do not have one, see the [Vue documentation](https://vuejs.org/guide/quick-start) to learn how to create it. - ### Installing from npm Start by installing the following packages: @@ -52,53 +46,35 @@ npm install ckeditor5-premium-features npm install @ckeditor/ckeditor5-vue ``` -With these packages installed, you now need to choose whether to install the `` component globally or locally and follow the appropriate instructions below. - -#### Registering the `` component globally - -To register the `` component globally, you must install the CKEditor 5 plugin for Vue. - -If you are using a plain Vue project, you should find the file where the `createApp` function is called and register the `CkeditorPlugin` plugin with the [`use()` method](https://vuejs.org/api/application.html#app-use). - -```js -import { createApp } from 'vue'; -import { CkeditorPlugin } from '@ckeditor/ckeditor5-vue'; -import App from './App.vue'; - -createApp( App ) - .use( CkeditorPlugin ) - .mount( '#app' ); -``` - -If you are using Nuxt.js, you can follow the [Nuxt.js documentation](https://nuxt.com/docs/guide/directory-structure/plugins#vue-plugins) to get access to the `use()` method and register this plugin. - -Now you can use the `` component in any of your Vue components. The following example shows a single file component with open source and premium plugins. +With these packages installed, create a new Vue component called `Editor.vue`. It will use the `` component to run the editor. The following example shows a single file component with open source and premium CKEditor 5 plugins. ```html ``` -#### Using the `` component locally - -If you do not want to enable the CKEditor 5 component globally, you can import the `Ckeditor` component from the `@ckeditor/ckeditor5-vue` package directly into the Vue component where you want to use it, and add it to the `components` object. +Now you can import and use the `Editor.vue` component anywhere in your application. ```html +``` - +```html + ``` ## Component directives From 8f399fc001f447ce1c19fe2c1e9dc92b4f64526d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gorzeli=C5=84ski?= Date: Mon, 9 Sep 2024 15:11:19 +0200 Subject: [PATCH 189/256] Docs: Transform the TSX snippet to JSX. --- .../integrations-cdn/react-multiroot-cdn.md | 132 ++++++++---------- 1 file changed, 60 insertions(+), 72 deletions(-) diff --git a/docs/getting-started/integrations-cdn/react-multiroot-cdn.md b/docs/getting-started/integrations-cdn/react-multiroot-cdn.md index 999f3b417c1..9a045437be5 100644 --- a/docs/getting-started/integrations-cdn/react-multiroot-cdn.md +++ b/docs/getting-started/integrations-cdn/react-multiroot-cdn.md @@ -37,19 +37,9 @@ npm install ckeditor5 @ckeditor/ckeditor5-react Use the `useMultiRootEditor` hook inside your project: -```tsx +```jsx import React from "react"; -import { useMultiRootEditor, withCKCloud } from "@ckeditor/ckeditor5-react"; -import { - useMultiRootEditor, - withCKEditorCloud, - type MultiRootHookProps, - type WithCKEditorCloudHocProps, -} from "@ckeditor/ckeditor5-react"; - -type EditorDemoProps = WithCKEditorCloudHocProps & { - data: Record; -}; +import { useMultiRootEditor, withCKEditorCloud } from "@ckeditor/ckeditor5-react"; const withCKCloud = withCKEditorCloud( { cloud: { @@ -66,39 +56,10 @@ const withCKCloud = withCKEditorCloud( { } ); const MultiRootEditorDemo = withCKCloud( - ( { data, cloud }: EditorDemoProps ): ReactNode => { - const { - MultiRootEditor: MultiRootEditorBase, - CloudServices, - Essentials, - CKFinderUploadAdapter, - Autoformat, - Bold, - Italic, - BlockQuote, - CKBox, - CKFinder, - EasyImage, - Heading, - Image, - ImageCaption, - ImageStyle, - ImageToolbar, - ImageUpload, - Indent, - Link, - List, - MediaEmbed, - Paragraph, - PasteFromOffice, - PictureEditing, - Table, - TableToolbar, - TextTransformation, - } = cloud.CKEditor; - - class MultiRootEditor extends MultiRootEditorBase { - public static override builtinPlugins = [ + ( { data, cloud } ) => { + const { + MultiRootEditor: MultiRootEditorBase, + CloudServices, Essentials, CKFinderUploadAdapter, Autoformat, @@ -107,7 +68,6 @@ const MultiRootEditorDemo = withCKCloud( BlockQuote, CKBox, CKFinder, - CloudServices, EasyImage, Heading, Image, @@ -123,31 +83,59 @@ const MultiRootEditorDemo = withCKCloud( PasteFromOffice, PictureEditing, TextTransformation, - ]; - - public static override defaultConfig = { - toolbar: { - items: [ - "undo", - "redo", - "|", - "heading", - "|", - "bold", - "italic", - "|", - "link", - "uploadImage", - "blockQuote", - "mediaEmbed", - "|", - "bulletedList", - "numberedList", - "outdent", - "indent", - ], - }, - }; + } = cloud.CKEditor; + + class MultiRootEditor extends MultiRootEditorBase { + static builtinPlugins = [ + Essentials, + CKFinderUploadAdapter, + Autoformat, + Bold, + Italic, + BlockQuote, + CKBox, + CKFinder, + CloudServices, + EasyImage, + Heading, + Image, + ImageCaption, + ImageStyle, + ImageToolbar, + ImageUpload, + Indent, + Link, + List, + MediaEmbed, + Paragraph, + PasteFromOffice, + PictureEditing, + TextTransformation, + ]; + + static defaultConfig = { + toolbar: { + items: [ + "undo", + "redo", + "|", + "heading", + "|", + "bold", + "italic", + "|", + "link", + "uploadImage", + "blockQuote", + "mediaEmbed", + "|", + "bulletedList", + "numberedList", + "outdent", + "indent", + ], + }, + }; } const { toolbarElement, editableElements } = useMultiRootEditor( { @@ -161,7 +149,7 @@ const MultiRootEditorDemo = withCKCloud( { editableElements }
); - } + } ); ``` From cf34b0fe1b54c282ecbd0efeb0a0179f2c895c23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gorzeli=C5=84ski?= Date: Mon, 9 Sep 2024 15:16:09 +0200 Subject: [PATCH 190/256] Docs: Unify the introduction paragraph. --- docs/getting-started/integrations/react-default-npm.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting-started/integrations/react-default-npm.md b/docs/getting-started/integrations/react-default-npm.md index eea153ec2fd..cd1b3063984 100644 --- a/docs/getting-started/integrations/react-default-npm.md +++ b/docs/getting-started/integrations/react-default-npm.md @@ -16,7 +16,7 @@ order: 10

-React lets you build user interfaces out of individual pieces called components. CKEditor 5 can be used as one of such components. +React lets you build user interfaces out of individual pieces called components. CKEditor 5 can be used as one of such components. This guide explains how to integrate CKEditor 5 into your React application using npm. Starting from version 6.0.0 of this package, you can use native type definitions provided by CKEditor 5. Check the details about {@link getting-started/setup/typescript-support TypeScript support}. From e4505dc1e88ab100e9a9792d0e1cb32a48451e13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gorzeli=C5=84ski?= Date: Mon, 9 Sep 2024 16:01:11 +0200 Subject: [PATCH 191/256] Docs: Unify quotes in the React CDN guide. --- .../integrations-cdn/react-default-cdn.md | 75 ++++++++++--------- 1 file changed, 38 insertions(+), 37 deletions(-) diff --git a/docs/getting-started/integrations-cdn/react-default-cdn.md b/docs/getting-started/integrations-cdn/react-default-cdn.md index 99b0d454392..b4d14873384 100644 --- a/docs/getting-started/integrations-cdn/react-default-cdn.md +++ b/docs/getting-started/integrations-cdn/react-default-cdn.md @@ -48,26 +48,26 @@ npm install @ckeditor/ckeditor5-react The `useCKEditorCloud` hook is responsible for returning information that: -* The editor is still downloading from the CDN with the `status = "loading"`. -* An error occurred during the download when `status = "error"`. Further information is in the error field. -* About the editor in the data field and its dependencies when `status = "success"`. +* The editor is still downloading from the CDN with the `status = 'loading'`. +* An error occurred during the download when `status = 'error'`. Further information is in the error field. +* About the editor in the data field and its dependencies when `status = 'success'`. Use the `` component inside your project. The below example shows how to use it with the open-source plugins. ```js -import React from "react"; -import { CKEditor, useCKEditorCloud } from "@ckeditor/ckeditor5-react"; +import React from 'react'; +import { CKEditor, useCKEditorCloud } from '@ckeditor/ckeditor5-react'; const CKEditorDemo = () => { const cloud = useCKEditorCloud( { - version: "{@var ckeditor5-version}", + version: '{@var ckeditor5-version}', } ); - if ( cloud.status === "error" ) { + if ( cloud.status === 'error' ) { return
Error!
; } - if ( cloud.status === "loading" ) { + if ( cloud.status === 'loading' ) { return
Loading...
; } @@ -83,10 +83,11 @@ const CKEditorDemo = () => { return ( Hello world!!!

" } + data={ '

Hello world!!!

' } config={ { + licenseKey: 'GPL', toolbar: { - items: [ "undo", "redo", "|", "bold", "italic" ], + items: [ 'undo', 'redo', '|', 'bold', 'italic' ], }, plugins: [ Bold, Essentials, Italic, Paragraph, Undo ], } } @@ -98,20 +99,20 @@ const CKEditorDemo = () => { To use premium plugins, set the `premium` property to `true` in the `useCKEditorCloud` configuration and provide your license key in the `CKEditor` configuration. ```js -import React from "react"; -import { CKEditor, useCKEditorCloud } from "@ckeditor/ckeditor5-react"; +import React from 'react'; +import { CKEditor, useCKEditorCloud } from '@ckeditor/ckeditor5-react'; const CKEditorDemo = () => { const cloud = useCKEditorCloud( { - version: "{@var ckeditor5-version}", + version: '{@var ckeditor5-version}', premium: true, } ); - if ( cloud.status === "error" ) { + if ( cloud.status === 'error' ) { return
Error!
; } - if ( cloud.status === "loading" ) { + if ( cloud.status === 'loading' ) { return
Loading...
; } @@ -130,11 +131,11 @@ const CKEditorDemo = () => { return ( Hello world!!!

" } + data={ '

Hello world!!!

' } config={ { - licenseKey: "", + licenseKey: '', toolbar: { - items: [ "undo", "redo", "|", "bold", "italic" ], + items: [ 'undo', 'redo', '|', 'bold', 'italic' ], }, plugins: [ Bold, @@ -156,24 +157,24 @@ const CKEditorDemo = () => { To use `CKBox`, specify the version and theme (optionally) in the `useCKEditorCloud` configuration. Also, remember about the actual plugin configuration inside `` component. ```js -import React from "react"; -import { CKEditor, useCKEditorCloud } from "@ckeditor/ckeditor5-react"; +import React from 'react'; +import { CKEditor, useCKEditorCloud } from '@ckeditor/ckeditor5-react'; const CKEditorDemo = () => { const cloud = useCKEditorCloud( { - version: "{@var ckeditor5-version}", + version: '{@var ckeditor5-version}', ckbox: { - version: "2.5.1", + version: '2.5.1', // Optional - it's already 'lark' by default. - theme: "lark", + theme: 'lark', }, } ); - if ( cloud.status === "error" ) { + if ( cloud.status === 'error' ) { return
Error!
; } - if ( cloud.status === "loading" ) { + if ( cloud.status === 'loading' ) { return
Loading...
; } @@ -192,10 +193,10 @@ const CKEditorDemo = () => { return ( Hello world!!!

" } + data={ '

Hello world!!!

' } config={ { toolbar: { - items: [ "undo", "redo", "|", "bold", "italic" ], + items: [ 'undo', 'redo', '|', 'bold', 'italic' ], }, plugins: [ Bold, @@ -208,9 +209,9 @@ const CKEditorDemo = () => { CKBoxImageEdit, ], ckbox: { - tokenUrl: "https://api.ckbox.io/token/demo", + tokenUrl: 'https://api.ckbox.io/token/demo', forceDemoLabel: true, - allowExternalImagesEditing: [ /^data:/, /^i.imgur.com\//, "origin" ], + allowExternalImagesEditing: [ /^data:/, /^i.imgur.com\//, 'origin' ], }, } } /> @@ -529,20 +530,20 @@ CKEditor 5 supports {@link getting-started/setup/ui-language multiple UI la Pass the language you need into the `translations` array inside the configuration in the `useCKEditorCloud`. ```js -import React from "react"; -import { CKEditor, useCKEditorCloud } from "@ckeditor/ckeditor5-react"; +import React from 'react'; +import { CKEditor, useCKEditorCloud } from '@ckeditor/ckeditor5-react'; const CKEditorDemo = () => { const cloud = useCKEditorCloud({ - version: "{@var ckeditor5-version}", - languages: [ "de" ] + version: '{@var ckeditor5-version}', + languages: [ 'de' ] }); - if ( cloud.status === "error" ) { + if ( cloud.status === 'error' ) { return
Error!
; } - if ( cloud.status === "loading" ) { + if ( cloud.status === 'loading' ) { return
Loading...
; } @@ -558,9 +559,9 @@ const CKEditorDemo = () => { return ( Hello world!!!

" } + data={ '

Hello world!!!

' } config={{ - toolbar: [ "undo", "redo", "|", "bold", "italic" ], + toolbar: [ 'undo', 'redo', '|', 'bold', 'italic' ], plugins: [ Bold, Essentials, Italic, Paragraph, Undo ], }} /> From 2d33e9a22a2709ad3e38cc21d7f2e77cb27768a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gorzeli=C5=84ski?= Date: Tue, 10 Sep 2024 09:43:21 +0200 Subject: [PATCH 192/256] Docs: Post-review fixes. --- .../integrations-cdn/react-default-cdn.md | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/docs/getting-started/integrations-cdn/react-default-cdn.md b/docs/getting-started/integrations-cdn/react-default-cdn.md index b4d14873384..afa5cce7888 100644 --- a/docs/getting-started/integrations-cdn/react-default-cdn.md +++ b/docs/getting-started/integrations-cdn/react-default-cdn.md @@ -38,7 +38,7 @@ You get ready-to-use code tailored to your needs! This guide assumes you have a React project. You can create a basic React project using [Vite](https://vitejs.dev/). Refer to the [React documentation](https://react.dev/learn/start-a-new-react-project) to learn how to set up a project in the framework. -### Installing react component from npm +### Installing React component from npm Install the `@ckeditor/ckeditor5-react` package: @@ -83,7 +83,7 @@ const CKEditorDemo = () => { return ( Hello world!!!

' } + data={ '

Hello world!

' } config={ { licenseKey: 'GPL', toolbar: { @@ -131,7 +131,7 @@ const CKEditorDemo = () => { return ( Hello world!!!

' } + data={ '

Hello world!

' } config={ { licenseKey: '', toolbar: { @@ -193,7 +193,7 @@ const CKEditorDemo = () => { return ( Hello world!!!

' } + data={ '

Hello world!

' } config={ { toolbar: { items: [ 'undo', 'redo', '|', 'bold', 'italic' ], @@ -225,7 +225,7 @@ There are various ways to use external plugins. Here is a list of them: * **Local UMD Plugins:** Dynamically import local UMD modules using the `import()` syntax. * **Local External Imports:** Load external plugins locally using additional bundler configurations (such as Vite). -* **CDN 3rd Party Plugins:** Load JavaScript and CSS files from a CDN by specifying the URLs. +* **CDN Third-Party Plugins:** Load JavaScript and CSS files from a CDN by specifying the URLs. * **Verbose Configuration:** Advanced plugin loading with options to specify both script and style sheet URLs, along with an optional `checkPluginLoaded` function to verify the plugin has been correctly loaded into the global scope. Here is an example: @@ -238,8 +238,8 @@ const CKEditorDemo = () => { const cloud = useCKEditorCloud( { version: '{@var ckeditor5-version}', plugins: { - PluginUMD: async () => import( './your-local-import.umd.js' ), - PluginLocalImport: async () => import( './your-local-import' ), + PluginUMD: async () => await import( './your-local-import.umd.js' ), + PluginLocalImport: async () => await import( './your-local-import' ), PluginThirdParty: [ 'https://cdn.example.com/plugin3.js', 'https://cdn.example.com/plugin3.css' @@ -396,28 +396,28 @@ function CKEditorNestedInstanceDemo( { name, content } ) { ], toolbar: { items: [ - 'undo', 'redo', - '|', 'heading', - '|', 'bold', 'italic', - '|', 'link', 'uploadImage', 'insertTable', 'blockQuote', 'mediaEmbed', - '|', 'bulletedList', 'numberedList', 'outdent', 'indent' + 'undo', 'redo', + '|', 'heading', + '|', 'bold', 'italic', + '|', 'link', 'uploadImage', 'insertTable', 'blockQuote', 'mediaEmbed', + '|', 'bulletedList', 'numberedList', 'outdent', 'indent' ] }, image: { toolbar: [ - 'imageStyle:inline', - 'imageStyle:block', - 'imageStyle:side', - '|', - 'toggleImageCaption', - 'imageTextAlternative' + 'imageStyle:inline', + 'imageStyle:block', + 'imageStyle:side', + '|', + 'toggleImageCaption', + 'imageTextAlternative' ] }, table: { contentToolbar: [ - 'tableColumn', - 'tableRow', - 'mergeTableCells' + 'tableColumn', + 'tableRow', + 'mergeTableCells' ] } } } @@ -559,7 +559,7 @@ const CKEditorDemo = () => { return ( Hello world!!!

' } + data={ '

Hello world!

' } config={{ toolbar: [ 'undo', 'redo', '|', 'bold', 'italic' ], plugins: [ Bold, Essentials, Italic, Paragraph, Undo ], From 4e8164aebb513fb1d1345ca38a68c7758f7bceb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gorzeli=C5=84ski?= Date: Tue, 10 Sep 2024 10:19:50 +0200 Subject: [PATCH 193/256] Docs: Restructurize the React CDN guide. --- .../integrations-cdn/react-default-cdn.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/getting-started/integrations-cdn/react-default-cdn.md b/docs/getting-started/integrations-cdn/react-default-cdn.md index afa5cce7888..071684f0597 100644 --- a/docs/getting-started/integrations-cdn/react-default-cdn.md +++ b/docs/getting-started/integrations-cdn/react-default-cdn.md @@ -38,7 +38,7 @@ You get ready-to-use code tailored to your needs! This guide assumes you have a React project. You can create a basic React project using [Vite](https://vitejs.dev/). Refer to the [React documentation](https://react.dev/learn/start-a-new-react-project) to learn how to set up a project in the framework. -### Installing React component from npm +### Installing the React component from npm Install the `@ckeditor/ckeditor5-react` package: @@ -52,6 +52,8 @@ The `useCKEditorCloud` hook is responsible for returning information that: * An error occurred during the download when `status = 'error'`. Further information is in the error field. * About the editor in the data field and its dependencies when `status = 'success'`. +### Using the component + Use the `` component inside your project. The below example shows how to use it with the open-source plugins. ```js @@ -96,6 +98,8 @@ const CKEditorDemo = () => { }; ``` +### Using the component with premium plugins + To use premium plugins, set the `premium` property to `true` in the `useCKEditorCloud` configuration and provide your license key in the `CKEditor` configuration. ```js @@ -152,7 +156,9 @@ const CKEditorDemo = () => { }; ``` -### Usage with CKBox +With the configuration in place, you can use the above `` elements as any other React component. + +### Using the component with CKBox To use `CKBox`, specify the version and theme (optionally) in the `useCKEditorCloud` configuration. Also, remember about the actual plugin configuration inside `` component. @@ -219,7 +225,7 @@ const CKEditorDemo = () => { }; ``` -### Usage with external plugins +### Using the component with external plugins There are various ways to use external plugins. Here is a list of them: From fecd2a4f0e5ecf15c389f0b62af430ab35c3f0fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gorzeli=C5=84ski?= Date: Tue, 10 Sep 2024 10:36:28 +0200 Subject: [PATCH 194/256] Docs: Change the heading of the multi-root guide. --- docs/getting-started/integrations-cdn/react-multiroot-cdn.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting-started/integrations-cdn/react-multiroot-cdn.md b/docs/getting-started/integrations-cdn/react-multiroot-cdn.md index 9a045437be5..b84289ce32f 100644 --- a/docs/getting-started/integrations-cdn/react-multiroot-cdn.md +++ b/docs/getting-started/integrations-cdn/react-multiroot-cdn.md @@ -9,7 +9,7 @@ modified_at: 2024-04-25 {@snippet installation/integrations/framework-integration} -# React rich text multi-root editor hook +# React rich text multi-root editor hook with CDN

From 37782245dadc6b3ca0fbe1b35d9a379da5b3bcbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gorzeli=C5=84ski?= Date: Tue, 10 Sep 2024 12:12:50 +0200 Subject: [PATCH 195/256] Docs: Post-review fixes. --- .../integrations-cdn/react-default-cdn.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/getting-started/integrations-cdn/react-default-cdn.md b/docs/getting-started/integrations-cdn/react-default-cdn.md index 071684f0597..a37c713e521 100644 --- a/docs/getting-started/integrations-cdn/react-default-cdn.md +++ b/docs/getting-started/integrations-cdn/react-default-cdn.md @@ -156,7 +156,13 @@ const CKEditorDemo = () => { }; ``` -With the configuration in place, you can use the above `` elements as any other React component. +With the configuration in place, you can use the above elements as any other React component. + +```html + + + +``` ### Using the component with CKBox @@ -533,7 +539,7 @@ It is not mandatory to build applications on top of the above samples, however, CKEditor 5 supports {@link getting-started/setup/ui-language multiple UI languages}, and so does the official React component. Follow the instructions below to translate CKEditor 5 in your React application. -Pass the language you need into the `translations` array inside the configuration in the `useCKEditorCloud`. +Pass the languages you need into the `languages` array inside the configuration of the `useCKEditorCloud` hook. ```js import React from 'react'; From e70f89de844686b23d9ae983ac677e17ce8cf0cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gorzeli=C5=84ski?= Date: Tue, 10 Sep 2024 13:59:45 +0200 Subject: [PATCH 196/256] Update docs/getting-started/integrations-cdn/react-default-cdn.md --- docs/getting-started/integrations-cdn/react-default-cdn.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting-started/integrations-cdn/react-default-cdn.md b/docs/getting-started/integrations-cdn/react-default-cdn.md index a37c713e521..f4fbb98efbd 100644 --- a/docs/getting-started/integrations-cdn/react-default-cdn.md +++ b/docs/getting-started/integrations-cdn/react-default-cdn.md @@ -130,7 +130,7 @@ const CKEditorDemo = () => { Undo, } = cloud.CKEditor; - const { SlashCommand } = cloud.CKEditorPremiumFeatures!; + const { SlashCommand } = cloud.CKEditorPremiumFeatures; return ( Date: Tue, 10 Sep 2024 14:15:36 +0200 Subject: [PATCH 197/256] Docs: Post-review fixes. --- .../integrations-cdn/angular.md | 195 +++++------------- 1 file changed, 57 insertions(+), 138 deletions(-) diff --git a/docs/getting-started/integrations-cdn/angular.md b/docs/getting-started/integrations-cdn/angular.md index 8ba3c3ee591..eff21293248 100644 --- a/docs/getting-started/integrations-cdn/angular.md +++ b/docs/getting-started/integrations-cdn/angular.md @@ -107,7 +107,7 @@ You get ready-to-use code tailored to your needs! This guide assumes you already have a Angular project. To create such a project, you can use Angular CLI. Refer to the [Angular documentation](https://angular.io/cli) to learn more. -### Installing Angular component from npm +### Installing the Angular component from npm Install the [CKEditor 5 WYSIWYG editor component for Angular](https://www.npmjs.com/package/@ckeditor/ckeditor5-angular): @@ -115,7 +115,61 @@ Install the [CKEditor 5 WYSIWYG editor component for Angular](https://www.n npm install @ckeditor/ckeditor5-angular ``` -This setup differs depending on the type of components you use. +### Using the Angular component + +To use CKEditor 5 with CDN, you need to import the `loadCKEditorCloud` function and call it inside `ngOnInit` with the `version` provided in the configuration. + +```js +import { Component } from '@angular/core'; +import { loadCKEditorCloud } from '@ckeditor/ckeditor5-angular'; + +@Component( { + selector: 'app-simple-cdn-usage', + templateUrl: './simple-cdn-usage.component.html', +} ) +export class SimpleCdnUsageComponent { + Editor = null; + + config = null; + + editorData = '

Hello world!

'; + + ngOnInit() { + loadCKEditorCloud({ + version: '{@var ckeditor5-version}', + premium: true + }).then( this.setupEditor.bind( this ) ); + } + + _setupEditor ( cloud ) { + const { + ClassicEditor, + Essentials, + Bold, + Italic, + Mention + } = cloud.CKEditor; + + const { SlashCommand } = cloud.CKEditorPremiumFeatures; + + this.Editor = ClassicEditor; + this.config = { + plugins: [ Bold, Essentials, Italic, Paragraph, Undo, Mention, SlashCommand ], + toolbar: { + items: [ 'undo', 'redo', '|', 'bold', 'italic' ], + } + }; + } +} +``` + +The `` component supports the following properties: + +* `version` (required) – The version of CKEditor Cloud Services to use. +* `languages` – The languages to load. English language ('en') should not be passed because it is already bundled in. +* `premium` – If `true` then the premium features will be loaded. +* `ckbox` – CKBox bundle configuration. +* `plugins` – Additional resources to load. ## Supported `@Input` properties @@ -230,6 +284,7 @@ export class MyComponent { version: '43.0.0', }).then( this._setupEditor.bind( this ) ); } + private _setupEditor( cloud ) { const { ClassicEditor @@ -355,142 +410,6 @@ Fired when the editor crashes. Once the editor is crashed, the internal watchdog Prior to ckeditor5-angular `v7.0.1`, this event was not fired for crashes during the editor initialization.
-## CDN - -To use CKEditor 5 with CDN, you need to import the `loadCKEditorCloud` function and call it inside `ngOnInit` with the `version` provided in the configuration. - -```js -import { Component } from '@angular/core'; -import { loadCKEditorCloud } from '@ckeditor/ckeditor5-angular'; - -@Component( { - selector: 'app-simple-cdn-usage', - templateUrl: './simple-cdn-usage.component.html', -} ) -export class SimpleCdnUsageComponent { - public Editor = null; - - public config = null; - - public editorData = `

Getting used to an entirely different culture can be challenging. - While it’s also nice to learn about cultures online or from books, nothing comes close to experiencing cultural diversity in person. - You learn to appreciate each and every single one of the differences while you become more culturally fluid.

`; - - public ngOnInit(): void { - loadCKEditorCloud({ - version: '43.0.0', - }).then( this.setupEditor.bind( this ) ); - } - - private _setupEditor (cloud: CKEditorCloudResult ) { - const { - ClassicEditor, - Essentials, - CKFinderUploadAdapter, - Autoformat, - Bold, - Italic, - BlockQuote, - CKBox, - CKFinder, - CloudServices, - EasyImage, - Heading, - Image, - ImageCaption, - ImageStyle, - ImageToolbar, - ImageUpload, - Indent, - Link, - List, - MediaEmbed, - Paragraph, - PasteFromOffice, - PictureEditing, - Table, - TableToolbar, - TextTransformation, - } = cloud.CKEditor; - - this.Editor = ClassicEditor; - this.config = { - plugins: [ - Essentials, - CKFinderUploadAdapter, - Autoformat, - Bold, - Italic, - BlockQuote, - CKBox, - CKFinder, - CloudServices, - EasyImage, - Heading, - Image, - ImageCaption, - ImageStyle, - ImageToolbar, - ImageUpload, - Indent, - Link, - List, - MediaEmbed, - Paragraph, - PasteFromOffice, - PictureEditing, - Table, - TableToolbar, - TextTransformation, - ], - toolbar: { - items: [ - 'undo', - 'redo', - '|', - 'heading', - '|', - 'bold', - 'italic', - '|', - 'link', - 'uploadImage', - 'insertTable', - 'blockQuote', - 'mediaEmbed', - '|', - 'bulletedList', - 'numberedList', - 'outdent', - 'indent', - ], - }, - image: { - toolbar: [ - 'imageStyle:inline', - 'imageStyle:block', - 'imageStyle:side', - '|', - 'toggleImageCaption', - 'imageTextAlternative', - ], - }, - table: { - contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ], - }, - }; - } -} -``` - -The `` component supports the following properties: - -* `version` (required) – The version of CKEditor Cloud Services to use. -* `languages` – The languages to load. English language ('en') should not be passed because it is already bundled in. -* `premium` – If `true` then the premium features will be loaded. -* `ckbox` – CKBox bundle configuration. -* `plugins` – Additional resources to load. - ## Integration with `ngModel` The component implements the [`ControlValueAccessor`](https://angular.io/api/forms/ControlValueAccessor) interface and works with the `ngModel`. Here is how to use it: From cc25783dd3ec60560f674fb26f0f056e516641a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gorzeli=C5=84ski?= Date: Tue, 10 Sep 2024 14:28:52 +0200 Subject: [PATCH 198/256] Docs: Fix snippet. --- .../integrations-cdn/angular.md | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/docs/getting-started/integrations-cdn/angular.md b/docs/getting-started/integrations-cdn/angular.md index eff21293248..fec3a0e8ea0 100644 --- a/docs/getting-started/integrations-cdn/angular.md +++ b/docs/getting-started/integrations-cdn/angular.md @@ -119,7 +119,7 @@ npm install @ckeditor/ckeditor5-angular To use CKEditor 5 with CDN, you need to import the `loadCKEditorCloud` function and call it inside `ngOnInit` with the `version` provided in the configuration. -```js +```ts import { Component } from '@angular/core'; import { loadCKEditorCloud } from '@ckeditor/ckeditor5-angular'; @@ -128,20 +128,20 @@ import { loadCKEditorCloud } from '@ckeditor/ckeditor5-angular'; templateUrl: './simple-cdn-usage.component.html', } ) export class SimpleCdnUsageComponent { - Editor = null; + public Editor = null; - config = null; + public config = null; - editorData = '

Hello world!

'; + public editorData = '

Hello world!

'; - ngOnInit() { - loadCKEditorCloud({ + public ngOnInit(): void { + loadCKEditorCloud( { version: '{@var ckeditor5-version}', premium: true - }).then( this.setupEditor.bind( this ) ); + } ).then( this.setupEditor.bind( this ) ); } - _setupEditor ( cloud ) { + private _setupEditor ( cloud: CKEditorCloudResult ) { const { ClassicEditor, Essentials, @@ -154,6 +154,7 @@ export class SimpleCdnUsageComponent { this.Editor = ClassicEditor; this.config = { + licenseKey: '', // Or 'GPL'. plugins: [ Bold, Essentials, Italic, Paragraph, Undo, Mention, SlashCommand ], toolbar: { items: [ 'undo', 'redo', '|', 'bold', 'italic' ], @@ -163,7 +164,7 @@ export class SimpleCdnUsageComponent { } ``` -The `` component supports the following properties: +The `` function supports the following properties: * `version` (required) – The version of CKEditor Cloud Services to use. * `languages` – The languages to load. English language ('en') should not be passed because it is already bundled in. From 40a2b3367eb4b55458d5b5c543dc0ded5c8ad0ca Mon Sep 17 00:00:00 2001 From: Filip Sobol Date: Tue, 10 Sep 2024 15:05:59 +0200 Subject: [PATCH 199/256] Docs: Add `Loading CDN resources` page. --- .../integrations-cdn/vuejs-v3.md | 4 +- .../setup/loading-cdn-resources.md | 120 ++++++++++++++++++ 2 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 docs/getting-started/setup/loading-cdn-resources.md diff --git a/docs/getting-started/integrations-cdn/vuejs-v3.md b/docs/getting-started/integrations-cdn/vuejs-v3.md index b918b73911d..1dff3273e88 100644 --- a/docs/getting-started/integrations-cdn/vuejs-v3.md +++ b/docs/getting-started/integrations-cdn/vuejs-v3.md @@ -55,7 +55,7 @@ export default { data() { return { cloud: useCKEditorCloud( { - version: '43.0.0', + version: '{@var ckeditor5-version}', premium: true } ), data: '

Hello world!

', @@ -100,6 +100,8 @@ export default { ``` +In the above example, the `useCKEditorCloud` helper is used to load the editor code and plugins from CDN. The `premium` option is set to also load premium plugins. For more information about the `useCKEditorCloud` helper, see the {@link getting-started/setup/loading-cdn-resources Loading CDN resources} page. + Now you can import and use the `Editor.vue` component anywhere in your application. ```html diff --git a/docs/getting-started/setup/loading-cdn-resources.md b/docs/getting-started/setup/loading-cdn-resources.md new file mode 100644 index 00000000000..033eecdc876 --- /dev/null +++ b/docs/getting-started/setup/loading-cdn-resources.md @@ -0,0 +1,120 @@ +--- +category: setup +meta-title: Loading CDN resources | CKEditor 5 documentation +meta-description: Learn how to load CKEditor 5 resources from CDN. +order: 130 +modified_at: 2024-09-10 +--- + +# Loading CDN resources + +Loading CKEditor 5 and its plugins from a CDN requires adding the necessary script and stylesheet tags to the `` of your page. In some environments, this can be done manually without much hassle by following the {@link getting-started/integrations-cdn/quick-start CDN for Vanilla JavaScript} guide. + +However, in other environments this may require more work. This is especially true if you want to load some resources conditionally or dynamically, or need to wait for the resources to be loaded before using them. + +For these reason, we provide `useCKEditorCloud` and `loadCKEditorCloud` helper functions to make this process easier. These functions will handle adding the necessary script and stylesheet tags to your page, ensure that the resources are only loaded once, and provide access to the data exported by them. This way you can load CKEditor 5 and its plugins from a CDN without worrying about the technical details. + +If you use our {@link getting-started/integrations-cdn/react-default-cdn React} and {@link getting-started/integrations-cdn/vuejs-v3 Vue.js 3+} integrations, see the {@link getting-started/setup/loading-cdn-resources#using-the-useckeditorcloud-function Using the `useCKEditorCloud` function} section. Otherwise, see the {@link getting-started/setup/loading-cdn-resources#using-the-loadckeditorcloud-function Using the `loadCKEditorCloud` function} section. + +## Using the `useCKEditorCloud` function + +Our {@link getting-started/integrations-cdn/react-default-cdn React} and {@link getting-started/integrations-cdn/vuejs-v3 Vue.js 3+} integrations export a helper functions named `useCKEditorCloud` to help you load CDN resources. These helpers are only small wrappers around the `loadCKEditorCloud` function, but are designed to better integrate with the specific framework, its lifecycle, and reactivity mechanisms. + +Here is an example of how you can use `useCKEditorCloud`: + +```js +const cloud = useCKEditorCloud( { + version: '{@var ckeditor5-version}', + premium: true +} ); +``` + +This will add the necessary script and stylesheet tags to the page's `` and update the internal state to reflect the loading status. Depending on the framework, the `useCKEditorCloud` function may return different values. Please refer to the documentation of the specific integration for more details. + +Regardless of the used framework, the `useCKEditorCloud` functions always accept the same options, which are described in the {@link getting-started/setup/loading-cdn-resources#the-loadckeditorcloud-function-options The `loadCKEditorCloud` function options} section. + +## Using the `loadCKEditorCloud` function + +To use `loadCKEditorCloud` helper, you need to first install the `@ckeditor/ckeditor5-integrations-common` package: + +```bash +npm install @ckeditor/ckeditor5-integrations-common +``` + +Then you can use the `loadCKEditorCloud` function like this: + +```js +import { loadCKEditorCloud } from '@ckeditor/ckeditor5-integrations-common'; + +const { CKEditor, CKEditorPremiumFeatures } = await loadCKEditorCloud( { + version: '{@var ckeditor5-version}', + premium: true +} ); +``` + +The `loadCKEditorCloud` function returns a promise that resolves to an object in which each key contains data of the corresponding CDN resources. The exact object shape depends on the options passed to the function. + +The options accepted by the `loadCKEditorCloud` function are described in the {@link getting-started/setup/loading-cdn-resources#the-loadckeditorcloud-function-options The `loadCKEditorCloud` function options} section. + +## The `loadCKEditorCloud` function options + +The `loadCKEditorCloud` function (and `useCKEditorCloud` functions which are small wrappers around it) accepts an object with the following properties: + +* `version` (required) – The version of CKEditor 5 and premium features (if `premium` option is set to `true`) to load. +* `languages` (optional) – An array of language codes to load translations for. +* `premium` (optional) – A boolean value that indicates whether to load premium plugins. [1] +* `ckbox` (optional) – Configuration for loading CKBox integration. [1] +* `plugins` (optional) – Configuration for loading additional plugins. The object should have the global plugin name as keys and the plugin configuration as values. [1] + + +[1] Using this option will result in additional network requests for JavaScript and CSS assets. Make sure to only use these options when you need them. + + + + +Here's an example showing all the available options: + +```javascript +{ + version: '{@var ckeditor5-version}', + languages: [ 'en', 'de' ], + premium: true, + ckbox: { + version: '2.5.1', + theme: 'lark' // Optional, default 'lark'. + }, + plugins: { + ThirdPartyPlugin: [ + 'https://cdn.example.com/plugin.umd.js', + 'https://cdn.example.com/plugin.css' + ], + AnotherPlugin: () => import( './path/to/plugin.umd.js' ), + YetAnotherPlugin: { + scripts: [ 'https://cdn.example.com/plugin.umd.js' ], + stylesheets: [ 'https://cdn.example.com/plugin.css' ], + + // Optional, if it's not passed then the name of the plugin will be used. + checkPluginLoaded: () => window.PLUGIN_NAME + } + } +} +``` + +Note that unless the `checkPluginLoaded` callback is used, the keys in the `plugins` object must match the names of the global object the plugins use. As shown in the example above, we used the `checkPluginLoaded` to be able to access the plugin using the `YetAnotherPlugin` key, while the plugin itself assigns to the `window.PLUGIN_NAME` property. + +With this configuration, the object returned by this function will have the following properties: + +* `CKEditor` – The base CKEditor 5 library. +* `CKEditorPremiumFeatures` – Premium features for CKEditor 5. This option is only available when `premium` is set to `true`. +* `CKBox` – The CKBox integration. This option is only available when the `ckbox` option is provided. +* `ThirdPartyPlugin` – The custom plugin registered in the `plugins` option. +* `AnotherPlugin` – The custom plugin registered in the `plugins` option. +* `YetAnotherPlugin` – The custom plugin registered in the `plugins` option. From b01d91855c79c44c80bbe925430ee52a2f388896 Mon Sep 17 00:00:00 2001 From: Filip Sobol Date: Tue, 10 Sep 2024 15:08:45 +0200 Subject: [PATCH 200/256] Apply suggestions from code review [short flow] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mateusz Gorzeliński --- docs/getting-started/integrations-cdn/vuejs-v3.md | 6 ++---- docs/getting-started/integrations/vuejs-v3.md | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/getting-started/integrations-cdn/vuejs-v3.md b/docs/getting-started/integrations-cdn/vuejs-v3.md index 1dff3273e88..61d4e5a83f1 100644 --- a/docs/getting-started/integrations-cdn/vuejs-v3.md +++ b/docs/getting-started/integrations-cdn/vuejs-v3.md @@ -61,7 +61,7 @@ export default { data: '

Hello world!

', config: { licenseKey: '', // Or "GPL" - toolbar: [ 'heading', '|', 'bold', 'italic' ] + toolbar: [ 'undo', 'redo', '|', 'bold', 'italic' ] } } }, @@ -75,7 +75,6 @@ export default { ClassicEditor, Paragraph, Essentials, - Heading, Bold, Italic, Mention @@ -87,7 +86,6 @@ export default { static builtinPlugins = [ Essentials, Paragraph, - Heading, Bold, Italic, Mention, @@ -102,7 +100,7 @@ export default { In the above example, the `useCKEditorCloud` helper is used to load the editor code and plugins from CDN. The `premium` option is set to also load premium plugins. For more information about the `useCKEditorCloud` helper, see the {@link getting-started/setup/loading-cdn-resources Loading CDN resources} page. -Now you can import and use the `Editor.vue` component anywhere in your application. +Now, you can import and use the `Editor.vue` component anywhere in your application. ```html