diff --git a/.changes/unreleased/tinymce-TINY-12001-2025-06-25.yaml b/.changes/unreleased/tinymce-TINY-12001-2025-06-25.yaml new file mode 100644 index 0000000000..eef28b031e --- /dev/null +++ b/.changes/unreleased/tinymce-TINY-12001-2025-06-25.yaml @@ -0,0 +1,6 @@ +project: tinymce +kind: Added +body: Support for TinyMCE 8 license keys and license key manager. +time: 2025-06-25T10:02:01.705807+10:00 +custom: + Issue: TINY-12001 diff --git a/.changes/unreleased/tinymce-TINY-12021-2025-04-09.yaml b/.changes/unreleased/tinymce-TINY-12021-2025-04-09.yaml new file mode 100644 index 0000000000..67da8cdd38 --- /dev/null +++ b/.changes/unreleased/tinymce-TINY-12021-2025-04-09.yaml @@ -0,0 +1,6 @@ +project: tinymce +kind: Added +body: New `pageUid` and `editorUid` properties to tinymce global and editor instance respectively. +time: 2025-04-09T13:00:36.019555+10:00 +custom: + Issue: TINY-12021 diff --git a/modules/katamari/src/main/ts/ephox/katamari/api/Id.ts b/modules/katamari/src/main/ts/ephox/katamari/api/Id.ts index 7f039e0e15..c666c8e582 100644 --- a/modules/katamari/src/main/ts/ephox/katamari/api/Id.ts +++ b/modules/katamari/src/main/ts/ephox/katamari/api/Id.ts @@ -1,3 +1,5 @@ +import * as IdUtils from '../util/IdUtils'; + import * as Num from './Num'; /** @@ -13,7 +15,7 @@ import * as Num from './Num'; */ let unique = 0; -export const generate = (prefix: string): string => { +const generate = (prefix: string): string => { const date = new Date(); const time = date.getTime(); const random = Math.floor(Num.random() * 1000000000); @@ -22,3 +24,21 @@ export const generate = (prefix: string): string => { return prefix + '_' + random + unique + String(time); }; + +/** + * Generate a uuidv4 string + * In accordance with RFC 4122 (https://datatracker.ietf.org/doc/html/rfc4122) + */ +const uuidV4 = (): `${string}-${string}-${string}-${string}-${string}` => { + + if (window.isSecureContext) { + return window.crypto.randomUUID(); + } else { + return IdUtils.uuidV4String(); + } +}; + +export { + generate, + uuidV4 +}; diff --git a/modules/katamari/src/main/ts/ephox/katamari/util/IdUtils.ts b/modules/katamari/src/main/ts/ephox/katamari/util/IdUtils.ts new file mode 100644 index 0000000000..6e72d69fc3 --- /dev/null +++ b/modules/katamari/src/main/ts/ephox/katamari/util/IdUtils.ts @@ -0,0 +1,36 @@ +/* eslint-disable no-bitwise */ + +const uuidV4Bytes = (): Uint8Array => { + const bytes = window.crypto.getRandomValues(new Uint8Array(16)); + + // https://tools.ietf.org/html/rfc4122#section-4.1.3 + // This will first bit mask away the most significant 4 bits (version octet) + // then mask in the v4 number we only care about v4 random version at this point so (byte & 0b00001111 | 0b01000000) + bytes[6] = bytes[6] & 15 | 64; + + // https://tools.ietf.org/html/rfc4122#section-4.1.1 + // This will first bit mask away the highest two bits then masks in the highest bit so (byte & 0b00111111 | 0b10000000) + // So it will set the Msb0=1 & Msb1=0 described by the "The variant specified in this document." row in the table + bytes[8] = bytes[8] & 63 | 128; + + return bytes; +}; + +const uuidV4String = (): `${string}-${string}-${string}-${string}-${string}` => { + const uuid = uuidV4Bytes(); + const getHexRange = (startIndex: number, endIndex: number) => { + let buff = ''; + for (let i = startIndex; i <= endIndex; ++i) { + const hexByte = uuid[i].toString(16).padStart(2, '0'); + buff += hexByte; + } + return buff; + }; + // RFC 4122 UUID format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + return `${getHexRange(0, 3)}-${getHexRange(4, 5)}-${getHexRange(6, 7)}-${getHexRange(8, 9)}-${getHexRange(10, 15)}`; +}; + +export { + uuidV4Bytes, + uuidV4String +}; diff --git a/modules/katamari/src/test/ts/atomic/api/data/IdTest.ts b/modules/katamari/src/test/ts/atomic/api/data/IdTest.ts index c2e4131921..12c2cc006b 100644 --- a/modules/katamari/src/test/ts/atomic/api/data/IdTest.ts +++ b/modules/katamari/src/test/ts/atomic/api/data/IdTest.ts @@ -1,20 +1,62 @@ -import { describe, it } from '@ephox/bedrock-client'; +import { describe, it, context } from '@ephox/bedrock-client'; import { assert } from 'chai'; import fc from 'fast-check'; import * as Id from 'ephox/katamari/api/Id'; +import * as IdUtils from 'ephox/katamari/util/IdUtils'; -describe('atomic.katamari.api.data.IdTest', () => () => { - it('Unit Tests', () => { - const one = Id.generate('test'); - const two = Id.generate('test'); - assert.equal(one.indexOf('test'), 0); - assert.equal(two.indexOf('test'), 0); - assert.notEqual(one, two); +describe('atomic.katamari.api.data.IdTest', () => { + context('generate', () => { + it('Unit Tests', () => { + const one = Id.generate('test'); + const two = Id.generate('test'); + assert.equal(one.indexOf('test'), 0); + assert.equal(two.indexOf('test'), 0); + assert.notEqual(one, two); + }); + + it('should not generate identical IDs', () => { + const arbId = fc.string(1, 30).map(Id.generate); + fc.assert(fc.property(arbId, arbId, (id1, id2) => id1 !== id2)); + }); }); - it('should not generate identical IDs', () => { - const arbId = fc.string(1, 30).map(Id.generate); - fc.assert(fc.property(arbId, arbId, (id1, id2) => id1 !== id2)); + context('uuidV4', () => { + const assertIsUuidV4 = (uuid: string): void => { + // From https://github.com/uuidjs/uuid/blob/main/src/regex.js + const v4Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + assert.isString(uuid); + assert.lengthOf(uuid, 36); + assert.match(uuid, v4Regex); + const version = parseInt(uuid.slice(14, 15), 16); + assert.equal(version, 4); + }; + + it('native uuids should be valid and unique', () => { + const length = 1000; + const uuids = Array.from({ length }, Id.uuidV4); + const [ one, two ] = uuids; + assertIsUuidV4(one); + assertIsUuidV4(two); + assert.notStrictEqual(one, two); + assert.lengthOf(new Set(uuids), length); + }); + + it('non-native uuids should be valid and unique', () => { + const length = 1000; + const uuids = Array.from({ length }, IdUtils.uuidV4String); + const [ one, two ] = uuids; + assertIsUuidV4(one); + assertIsUuidV4(two); + assert.notStrictEqual(one, two); + assert.lengthOf(new Set(uuids), length); + }); + + // https://datatracker.ietf.org/doc/html/rfc4122#section-4.4 + it('uuidV4Bytes should have correct fields set from section 4.4', () => { + const bytes = IdUtils.uuidV4Bytes(); + const bits = bytes[8].toString(2).padStart(8, '0'); + assert.strictEqual(bits.substring(0, 2), '10'); + }); }); }); diff --git a/modules/tinymce/src/core/demo/ts/demo/FullDemo.ts b/modules/tinymce/src/core/demo/ts/demo/FullDemo.ts index cfc1c44550..cfdfb4ac67 100644 --- a/modules/tinymce/src/core/demo/ts/demo/FullDemo.ts +++ b/modules/tinymce/src/core/demo/ts/demo/FullDemo.ts @@ -109,6 +109,7 @@ export default (): void => { }, image_caption: true, theme: 'silver', + license_key: 'gpl', setup: (ed) => { makeSidebar(ed, 'sidebar1', 'green', 200); makeSidebar(ed, 'sidebar2', 'green', 200); diff --git a/modules/tinymce/src/core/main/ts/ErrorReporter.ts b/modules/tinymce/src/core/main/ts/ErrorReporter.ts index 4a29408a2d..3dabb73e53 100644 --- a/modules/tinymce/src/core/main/ts/ErrorReporter.ts +++ b/modules/tinymce/src/core/main/ts/ErrorReporter.ts @@ -53,6 +53,10 @@ const modelLoadError = (editor: Editor, url: string, name: string): void => { logError(editor, 'ModelLoadError', createLoadError('model', url, name)); }; +const licenseKeyManagerLoadError = (editor: Editor, url: string, name: string): void => { + logError(editor, 'LicenseKeyManagerLoadError', createLoadError('license key manager', url, name)); +}; + const pluginInitError = (editor: Editor, name: string, err: any): void => { const message = I18n.translate([ 'Failed to initialize plugin: {0}', name ]); fireError(editor, 'PluginLoadError', { message }); @@ -77,6 +81,7 @@ export { languageLoadError, themeLoadError, modelLoadError, + licenseKeyManagerLoadError, pluginInitError, uploadError, displayError, diff --git a/modules/tinymce/src/core/main/ts/api/Editor.ts b/modules/tinymce/src/core/main/ts/api/Editor.ts index 52d3bfe908..0aebb96f86 100644 --- a/modules/tinymce/src/core/main/ts/api/Editor.ts +++ b/modules/tinymce/src/core/main/ts/api/Editor.ts @@ -1,4 +1,4 @@ -import { Arr, Fun, Type } from '@ephox/katamari'; +import { Arr, Fun, Type, Id } from '@ephox/katamari'; import * as EditorContent from '../content/EditorContent'; import * as Deprecations from '../Deprecations'; @@ -6,6 +6,7 @@ import * as NodeType from '../dom/NodeType'; import * as EditorRemove from '../EditorRemove'; import { BlobInfoImagePair } from '../file/ImageScanner'; import * as EditorFocus from '../focus/EditorFocus'; +import { LicenseKeyManager } from '../init/LicenseKeyManager'; import * as Render from '../init/Render'; import { type UserLookup, createUserLookup } from '../lookup/UserLookup'; import * as EditableRoot from '../mode/EditableRoot'; @@ -98,6 +99,14 @@ class Editor implements EditorObservable { */ public id: string; + /** + * A uuid string to uniquely identify an editor across any page. + * + * @property editorUid + * @type String + */ + public editorUid: string; + /** * Name/Value object containing plugin instances. * @@ -243,6 +252,7 @@ class Editor implements EditorObservable { public model!: Model; public undoManager!: UndoManager; public windowManager!: WindowManager; + public licenseKeyManager!: LicenseKeyManager; public _beforeUnload: (() => void) | undefined; public _eventDispatcher: EventDispatcher | undefined; public _nodeChangeDispatcher!: NodeChange; @@ -279,6 +289,7 @@ class Editor implements EditorObservable { const self = this; this.id = id; + this.editorUid = Id.uuidV4(); this.hidden = false; const normalizedOptions = normalizeOptions(editorManager.defaultOptions, options); @@ -350,6 +361,13 @@ class Editor implements EditorObservable { this.mode = createMode(self); + // Lock certain properties to reduce misuse + Object.defineProperty(this, 'editorUid', { + writable: false, + configurable: false, + enumerable: true, + }); + // Call setup editorManager.dispatch('SetupEditor', { editor: this }); const setupCallback = Options.getSetupCallback(self); diff --git a/modules/tinymce/src/core/main/ts/api/EditorManager.ts b/modules/tinymce/src/core/main/ts/api/EditorManager.ts index ae9f68ee66..1c05f9b852 100644 --- a/modules/tinymce/src/core/main/ts/api/EditorManager.ts +++ b/modules/tinymce/src/core/main/ts/api/EditorManager.ts @@ -1,7 +1,8 @@ -import { Arr, Obj, Type } from '@ephox/katamari'; +import { Arr, Obj, Type, Id } from '@ephox/katamari'; import * as ErrorReporter from '../ErrorReporter'; import * as FocusController from '../focus/FocusController'; +import LicenseKeyManagerLoader, { LicenseKeyManagerAddon } from '../init/LicenseKeyManager'; import AddOnManager from './AddOnManager'; import DOMUtils from './dom/DOMUtils'; @@ -100,6 +101,7 @@ interface EditorManager extends Observable { documentBaseURL: string; i18n: I18n; suffix: string; + pageUid: string; add (this: EditorManager, editor: Editor): Editor; addI18n: (code: string, item: Record) => void; @@ -118,6 +120,7 @@ interface EditorManager extends Observable { translate: (text: Untranslated) => TranslatedString; triggerSave: () => void; _setBaseUrl (this: EditorManager, baseUrl: string): void; + _addLicenseKeyManager (this: EditorManager, addOn: LicenseKeyManagerAddon): void; } const isQuirksMode = document.compatMode !== 'CSS1Compat'; @@ -139,6 +142,14 @@ const EditorManager: EditorManager = { documentBaseURL: null as any, suffix: null as any, + /** + * A uuid string to anonymously identify the page tinymce is loaded in + * + * @property pageUid + * @type String + */ + pageUid: Id.uuidV4(), + /** * Major version of TinyMCE build. * @@ -276,6 +287,17 @@ const EditorManager: EditorManager = { self.suffix = suffix; FocusController.setup(self); + + // Lock certain properties to reduce misuse + Arr.each( + [ 'majorVersion', 'minorVersion', 'releaseDate', 'pageUid', '_addLicenseKeyManager' ], + (property) => + Object.defineProperty(self, property, { + writable: false, + configurable: false, + enumerable: true, + }) + ); }, /** @@ -736,7 +758,9 @@ const EditorManager: EditorManager = { _setBaseUrl(baseUrl: string) { this.baseURL = new URI(this.documentBaseURL).toAbsolute(baseUrl.replace(/\/+$/, '')); this.baseURI = new URI(this.baseURL); - } + }, + + _addLicenseKeyManager: (addOn: LicenseKeyManagerAddon) => LicenseKeyManagerLoader.add(addOn), }; EditorManager.setup(); diff --git a/modules/tinymce/src/core/main/ts/init/Init.ts b/modules/tinymce/src/core/main/ts/init/Init.ts index 99b2026466..ab6e3bd665 100644 --- a/modules/tinymce/src/core/main/ts/init/Init.ts +++ b/modules/tinymce/src/core/main/ts/init/Init.ts @@ -20,6 +20,7 @@ import * as Disabled from '../mode/Disabled'; import { appendContentCssFromSettings } from './ContentCss'; import * as InitContentBody from './InitContentBody'; import * as InitIframe from './InitIframe'; +import LicenseKeyManagerLoader from './LicenseKeyManager'; const DOM = DOMUtils.DOM; @@ -122,6 +123,10 @@ const initModel = (editor: Editor) => { editor.model = Model(editor, ModelManager.urls[model]); }; +const initLicenseKeyManager = (editor: Editor) => { + LicenseKeyManagerLoader.init(editor); +}; + const renderFromLoadedTheme = (editor: Editor) => { // Render UI const render = editor.theme.renderUI; @@ -203,6 +208,7 @@ const init = async (editor: Editor): Promise => { initTooltipClosing(editor); initTheme(editor); initModel(editor); + initLicenseKeyManager(editor); initPlugins(editor); const renderInfo = await renderThemeUi(editor); augmentEditorUiApi(editor, Optional.from(renderInfo.api).getOr({})); diff --git a/modules/tinymce/src/core/main/ts/init/InitContentBody.ts b/modules/tinymce/src/core/main/ts/init/InitContentBody.ts index 0e6f9d4780..e6fd713e97 100644 --- a/modules/tinymce/src/core/main/ts/init/InitContentBody.ts +++ b/modules/tinymce/src/core/main/ts/init/InitContentBody.ts @@ -40,7 +40,6 @@ import * as TextPattern from '../textpatterns/TextPatterns'; import Quirks from '../util/Quirks'; import * as ContentCss from './ContentCss'; -import * as LicenseKeyValidation from './LicenseKeyValidation'; declare const escape: any; declare let tinymce: TinyMCE; @@ -489,8 +488,6 @@ const contentBodyLoaded = (editor: Editor): void => { preInit(editor); - LicenseKeyValidation.validateEditorLicenseKey(editor); - setupRtcThunk.fold(() => { const cancelProgress = startProgress(editor); // eslint-disable-next-line @typescript-eslint/no-floating-promises diff --git a/modules/tinymce/src/core/main/ts/init/LicenseKeyManager.ts b/modules/tinymce/src/core/main/ts/init/LicenseKeyManager.ts new file mode 100644 index 0000000000..a77c471f3d --- /dev/null +++ b/modules/tinymce/src/core/main/ts/init/LicenseKeyManager.ts @@ -0,0 +1,107 @@ +import { Fun, Obj, Type } from '@ephox/katamari'; + +import AddOnManager, { AddOnConstructor } from '../api/AddOnManager'; +import Editor from '../api/Editor'; +import * as Options from '../api/Options'; +import * as ErrorReporter from '../ErrorReporter'; + +export type LicenseKeyManagerAddon = AddOnConstructor; + +interface LicenseKeyManagerLoader { + readonly load: (editor: Editor, suffix: string) => void; + readonly isLoaded: (editor: Editor) => boolean; + readonly add: (addOn: LicenseKeyManagerAddon) => void; + readonly init: (editor: Editor) => void; +} + +interface ValidateData { + plugin?: string; + [key: string]: any; +} + +export interface LicenseKeyManager { + readonly validate: (data: ValidateData) => Promise; +} + +const GplLicenseKeyManager: LicenseKeyManager = { + validate: (data) => { + const { plugin } = data; + // Premium plugins are not allowed if 'gpl' is given as the license_key + return Promise.resolve(!Type.isString(plugin)); + }, +}; + +const ADDON_KEY = 'manager'; +const PLUGIN_CODE = 'licensekeymanager'; + +const setup = (): LicenseKeyManagerLoader => { + const addOnManager = AddOnManager(); + + const add = (addOn: LicenseKeyManagerAddon): void => { + addOnManager.add(ADDON_KEY, addOn); + }; + + const load = (editor: Editor, suffix: string): void => { + const licenseKey = Options.getLicenseKey(editor); + const plugins = new Set(Options.getPlugins(editor)); + const hasApiKey = Type.isString(Options.getApiKey(editor)); + + // Early return if addonConstructor already exists + if (Obj.has(addOnManager.urls, ADDON_KEY)) { + return; + } + + // Try loading external license key manager when + // - license_key is not 'gpl'; or + // - an API key is present; or + // - the licensekeymanager has been explicity listed, most likely through the forced_plugins option + if (licenseKey?.toLowerCase() !== 'gpl' || hasApiKey || plugins.has(PLUGIN_CODE)) { + const url = `plugins/${PLUGIN_CODE}/plugin${suffix}.js`; + addOnManager.load(ADDON_KEY, url).catch(() => { + ErrorReporter.licenseKeyManagerLoadError(editor, url, ADDON_KEY); + }); + } else { + add(Fun.constant(GplLicenseKeyManager)); + } + }; + + const isLoaded = (_editor: Editor): boolean => { + return Type.isNonNullable(addOnManager.get(ADDON_KEY)); + }; + + const init = (editor: Editor): void => { + const setLicenseKeyManager = (licenseKeyManager: LicenseKeyManager) => { + Object.defineProperty(editor, 'licenseKeyManager', { + value: licenseKeyManager, + writable: false, + configurable: false, + enumerable: true, + }); + }; + + // editor will not load without a license key manager constructor + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const LicenseKeyManager = addOnManager.get(ADDON_KEY)!; + + const licenseKeyManagerApi = LicenseKeyManager(editor, addOnManager.urls[ADDON_KEY]); + setLicenseKeyManager(licenseKeyManagerApi); + + // Validation of the license key is done asynchronously and does + // not block initialization of the editor + // The validate function is expected to set the editor to the correct + // state depending on if the license key is valid or not + // eslint-disable-next-line @typescript-eslint/no-floating-promises + editor.licenseKeyManager.validate({}); + }; + + return { + load, + isLoaded, + add, + init + }; +}; + +const LicenseKeyManagerLoader: LicenseKeyManagerLoader = setup(); + +export default LicenseKeyManagerLoader; diff --git a/modules/tinymce/src/core/main/ts/init/LicenseKeyValidation.ts b/modules/tinymce/src/core/main/ts/init/LicenseKeyValidation.ts deleted file mode 100644 index 267bc0d72c..0000000000 --- a/modules/tinymce/src/core/main/ts/init/LicenseKeyValidation.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Type } from '@ephox/katamari'; - -import Editor from '../api/Editor'; -import * as Options from '../api/Options'; - -type KeyStatus = 'VALID' | 'INVALID'; - -const isGplKey = (key: string) => key.toLowerCase() === 'gpl'; - -const isValidGeneratedKey = (key: string): boolean => key.length >= 64 && key.length <= 255; - -export const validateLicenseKey = (key: string): KeyStatus => isGplKey(key) || isValidGeneratedKey(key) ? 'VALID' : 'INVALID'; - -export const validateEditorLicenseKey = (editor: Editor): void => { - const licenseKey = Options.getLicenseKey(editor); - const hasApiKey = Type.isString(Options.getApiKey(editor)); - if (!hasApiKey && (Type.isUndefined(licenseKey) || validateLicenseKey(licenseKey) === 'INVALID')) { - // eslint-disable-next-line no-console - console.warn(`TinyMCE is running in evaluation mode. Provide a valid license key or add license_key: 'gpl' to the init config to agree to the open source license terms. Read more at https://www.tiny.cloud/license-key/`); - } -}; diff --git a/modules/tinymce/src/core/main/ts/init/Render.ts b/modules/tinymce/src/core/main/ts/init/Render.ts index e078098d2b..f2686d684c 100644 --- a/modules/tinymce/src/core/main/ts/init/Render.ts +++ b/modules/tinymce/src/core/main/ts/init/Render.ts @@ -20,6 +20,7 @@ import * as StyleSheetLoaderRegistry from '../dom/StyleSheetLoaderRegistry'; import * as ErrorReporter from '../ErrorReporter'; import * as Init from './Init'; +import LicenseKeyManagerLoader from './LicenseKeyManager'; interface UrlMeta { readonly url: string; @@ -68,6 +69,10 @@ const loadModel = (editor: Editor, suffix: string): void => { } }; +const loadLicenseKeyManager = (editor: Editor, suffix: string): void => { + LicenseKeyManagerLoader.load(editor, suffix); +}; + const getIconsUrlMetaFromUrl = (editor: Editor): Optional => Optional.from(Options.getIconsUrl(editor)) .filter(Strings.isNotEmpty) .map((url) => ({ @@ -95,6 +100,12 @@ const loadIcons = (scriptLoader: ScriptLoader, editor: Editor, suffix: string) = const loadPlugins = (editor: Editor, suffix: string) => { const loadPlugin = (name: string, url: string) => { + // If licensekeymanager is included in the plugins list + // or through external_plugins, skip it + if (name === 'licensekeymanager') { + return; + } + PluginManager.load(name, url).catch(() => { ErrorReporter.pluginLoadError(editor, url, name); }); @@ -124,13 +135,22 @@ const isModelLoaded = (editor: Editor): boolean => { return Type.isNonNullable(ModelManager.get(model)); }; +const isLicenseKeyManagerLoaded = (editor: Editor): boolean => { + return LicenseKeyManagerLoader.isLoaded(editor); +}; + const loadScripts = (editor: Editor, suffix: string) => { const scriptLoader = ScriptLoader.ScriptLoader; const initEditor = () => { - // If the editor has been destroyed or the theme and model haven't loaded then + // If the editor has been destroyed or the theme, model, licenseKeyManager haven't loaded then // don't continue to load the editor - if (!editor.removed && isThemeLoaded(editor) && isModelLoaded(editor)) { + if ( + !editor.removed && + isThemeLoaded(editor) && + isModelLoaded(editor) && + isLicenseKeyManagerLoaded(editor) + ) { // eslint-disable-next-line @typescript-eslint/no-floating-promises Init.init(editor); } @@ -138,6 +158,7 @@ const loadScripts = (editor: Editor, suffix: string) => { loadTheme(editor, suffix); loadModel(editor, suffix); + loadLicenseKeyManager(editor, suffix); loadLanguage(scriptLoader, editor); loadIcons(scriptLoader, editor, suffix); loadPlugins(editor, suffix); diff --git a/modules/tinymce/src/core/test/ts/browser/EditorAutoFocusTest.ts b/modules/tinymce/src/core/test/ts/browser/EditorAutoFocusTest.ts index bc20fb3bb0..6debdc0f4a 100644 --- a/modules/tinymce/src/core/test/ts/browser/EditorAutoFocusTest.ts +++ b/modules/tinymce/src/core/test/ts/browser/EditorAutoFocusTest.ts @@ -42,6 +42,7 @@ describe.skip('browser.tinymce.core.EditorAutoFocusTest', () => { EditorManager.init({ selector: 'div.tinymce', base_url: '/project/tinymce/js/tinymce/', + license_key: 'gpl', menubar: false, statusbar: false, height, diff --git a/modules/tinymce/src/core/test/ts/browser/EditorManagerCommandsTest.ts b/modules/tinymce/src/core/test/ts/browser/EditorManagerCommandsTest.ts index dbf3cafbec..c31af20606 100644 --- a/modules/tinymce/src/core/test/ts/browser/EditorManagerCommandsTest.ts +++ b/modules/tinymce/src/core/test/ts/browser/EditorManagerCommandsTest.ts @@ -5,6 +5,7 @@ import { assert } from 'chai'; import 'tinymce'; import Editor from 'tinymce/core/api/Editor'; import EditorManager from 'tinymce/core/api/EditorManager'; +import Model from 'tinymce/models/dom/Model'; import Theme from 'tinymce/themes/silver/Theme'; import * as ViewBlock from '../module/test/ViewBlock'; @@ -14,6 +15,7 @@ describe('browser.tinymce.core.EditorManagerCommandsTest', () => { before(() => { Theme(); + Model(); EditorManager._setBaseUrl('/project/tinymce/js/tinymce'); }); @@ -38,6 +40,7 @@ describe('browser.tinymce.core.EditorManagerCommandsTest', () => { viewBlock.update(''); await EditorManager.init({ selector: 'textarea.tinymce', + license_key: 'gpl', init_instance_callback: (editor1) => { assert.isFalse(editor1.isHidden(), 'editor should be visible'); EditorManager.execCommand('mceToggleEditor', false, test.value); @@ -53,6 +56,7 @@ describe('browser.tinymce.core.EditorManagerCommandsTest', () => { viewBlock.update(''); await EditorManager.init({ selector: 'textarea.tinymce', + license_key: 'gpl', init_instance_callback: (_editor1) => { assert.lengthOf(EditorManager.get(), 1); EditorManager.execCommand('mceRemoveEditor', false, test.value); @@ -67,12 +71,14 @@ describe('browser.tinymce.core.EditorManagerCommandsTest', () => { viewBlock.update(''); await EditorManager.init({ selector: 'textarea#ed_1', + license_key: 'gpl', init_instance_callback: (_editor1) => { assert.lengthOf(EditorManager.get(), 1); assert.isFalse(EditorManager.get('ed_1')?.mode.isReadOnly()); EditorManager.execCommand('mceAddEditor', false, { id: 'ed_2', options: { + license_key: 'gpl', readonly: true, init_instance_callback: (editor2: Editor) => { assert.lengthOf(EditorManager.get(), 2); diff --git a/modules/tinymce/src/core/test/ts/browser/EditorManagerSetupTest.ts b/modules/tinymce/src/core/test/ts/browser/EditorManagerSetupTest.ts index e7cbfe6b1e..8fc72f2849 100644 --- a/modules/tinymce/src/core/test/ts/browser/EditorManagerSetupTest.ts +++ b/modules/tinymce/src/core/test/ts/browser/EditorManagerSetupTest.ts @@ -33,8 +33,10 @@ describe('browser.tinymce.core.EditorManagerSetupTest', () => { it('script baseURL and suffix with script in svg', () => { viewBlock.update(''); + const pageUid = EditorManager.pageUid; EditorManager.setup(); assert.equal(EditorManager.baseURL, 'http://localhost/nonexistant', 'BaseURL is interpreted from the script src'); assert.equal(EditorManager.suffix, '.min', 'Suffix is interpreted from the script src'); + assert.strictEqual(EditorManager.pageUid, pageUid, 'pageUid should not change after calling setup'); }); }); diff --git a/modules/tinymce/src/core/test/ts/browser/EditorManagerTest.ts b/modules/tinymce/src/core/test/ts/browser/EditorManagerTest.ts index 66ac2de3b0..4ce08233e8 100644 --- a/modules/tinymce/src/core/test/ts/browser/EditorManagerTest.ts +++ b/modules/tinymce/src/core/test/ts/browser/EditorManagerTest.ts @@ -1,4 +1,5 @@ import { after, afterEach, before, describe, it } from '@ephox/bedrock-client'; +import { Obj } from '@ephox/katamari'; import { Remove, Selectors } from '@ephox/sugar'; import { assert } from 'chai'; import 'tinymce'; @@ -8,14 +9,21 @@ import Editor from 'tinymce/core/api/Editor'; import EditorManager from 'tinymce/core/api/EditorManager'; import PluginManager from 'tinymce/core/api/PluginManager'; import Tools from 'tinymce/core/api/util/Tools'; +import Model from 'tinymce/models/dom/Model'; +import Theme from 'tinymce/themes/silver/Theme'; +import * as UuidUtils from '../module/test/UuidUtils'; import * as ViewBlock from '../module/test/ViewBlock'; describe('browser.tinymce.core.EditorManagerTest', () => { const viewBlock = ViewBlock.bddSetup(); before(() => { + Theme(); + Model(); EditorManager._setBaseUrl('/project/tinymce/js/tinymce'); + // Check pageUid is defined before any editors are created + UuidUtils.assertIsUuid(EditorManager.pageUid); }); after(() => { @@ -34,6 +42,7 @@ describe('browser.tinymce.core.EditorManagerTest', () => { // eslint-disable-next-line @typescript-eslint/no-floating-promises EditorManager.init({ selector: 'textarea.tinymce', + license_key: 'gpl', init_instance_callback: (editor1) => { assert.lengthOf(EditorManager.get(), 1); assert.equal(EditorManager.get(0), EditorManager.activeEditor); @@ -58,6 +67,7 @@ describe('browser.tinymce.core.EditorManagerTest', () => { // eslint-disable-next-line @typescript-eslint/no-floating-promises EditorManager.init({ selector: '#' + (EditorManager.activeEditor as Editor).id, + license_key: 'gpl', }); assert.lengthOf(EditorManager.get(), 1); @@ -88,6 +98,7 @@ describe('browser.tinymce.core.EditorManagerTest', () => { EditorManager.init({ selector: 'textarea', language: langCode, + license_key: 'gpl', language_url: langUrl, init_instance_callback: (_ed) => { const scripts = Tools.grep(document.getElementsByTagName('script'), (script) => { @@ -107,6 +118,7 @@ describe('browser.tinymce.core.EditorManagerTest', () => { // eslint-disable-next-line @typescript-eslint/no-floating-promises EditorManager.init({ selector: 'textarea', + license_key: 'gpl', init_instance_callback: (editor1) => { setTimeout(() => { // Destroy the editor by setting innerHTML common ajax pattern @@ -119,6 +131,7 @@ describe('browser.tinymce.core.EditorManagerTest', () => { // eslint-disable-next-line @typescript-eslint/no-floating-promises EditorManager.init({ selector: 'textarea', + license_key: 'gpl', skin_url: '/project/tinymce/js/tinymce/skins/ui/oxide', content_css: '/project/tinymce/js/tinymce/skins/content/default', init_instance_callback: (editor2) => { @@ -163,6 +176,7 @@ describe('browser.tinymce.core.EditorManagerTest', () => { assert.deepEqual(new Editor('ed2', { base_url: '/project/tinymce/js/tinymce', + license_key: 'gpl', external_plugins: { plugina: '//domain/plugina2.js', pluginc: '//domain/pluginc.js' @@ -177,7 +191,8 @@ describe('browser.tinymce.core.EditorManagerTest', () => { }); assert.deepEqual(new Editor('ed3', { - base_url: '/project/tinymce/js/tinymce' + base_url: '/project/tinymce/js/tinymce', + license_key: 'gpl', }, EditorManager).options.get('external_plugins'), { plugina: '//domain/plugina.js', pluginb: '//domain/pluginb.js' @@ -202,6 +217,7 @@ describe('browser.tinymce.core.EditorManagerTest', () => { // eslint-disable-next-line @typescript-eslint/no-floating-promises EditorManager.init({ selector: invalidName + '.targetEditor', + license_key: 'gpl', inline: true }); @@ -209,4 +225,46 @@ describe('browser.tinymce.core.EditorManagerTest', () => { DOMUtils.DOM.remove(elm); }); }); + + it('TINY-12021: pageUid', async () => { + const pageUid = EditorManager.pageUid; + UuidUtils.assertIsUuid(pageUid); + + viewBlock.update(''); + await EditorManager.init({ + selector: 'textarea.tinymce', + license_key: 'gpl', + setup: (editor) => { + assert.equal(editor.editorManager.pageUid, pageUid); + }, + }); + }); + + it('TINY-12020: EditorManager should have correct locked properties', async () => { + const lockedEditorManagerProperties = [ 'majorVersion', 'minorVersion', 'releaseDate', 'pageUid', '_addLicenseKeyManager' ] as const; + const lockedPropertiesSet = new Set(lockedEditorManagerProperties); + + const descriptors = Object.getOwnPropertyDescriptors(EditorManager); + Obj.each(descriptors, (descriptor, key) => { + if (lockedPropertiesSet.has(key)) { + assert.isFalse(descriptor.configurable, `${key} should not be configurable`); + assert.isFalse(descriptor.writable, `${key} should not be writable`); + assert.isTrue(descriptor.enumerable, `${key} should be enumerable`); + } else { + assert.isTrue(descriptor.configurable, `${key} should be configurable`); + assert.isTrue(descriptor.writable, `${key} should be writable`); + assert.isTrue(descriptor.enumerable, `${key} should be enumerable`); + } + }); + + for (const property of lockedEditorManagerProperties) { + assert.throws(() => { + (EditorManager[property] as any) = 'some_random_value'; + }); + assert.throws(() => { + delete EditorManager[property]; + }); + assert.notStrictEqual(EditorManager[property], 'some_random_value'); + } + }); }); diff --git a/modules/tinymce/src/core/test/ts/browser/EditorRemoveTest.ts b/modules/tinymce/src/core/test/ts/browser/EditorRemoveTest.ts index 785072f3fd..80bb9923ad 100644 --- a/modules/tinymce/src/core/test/ts/browser/EditorRemoveTest.ts +++ b/modules/tinymce/src/core/test/ts/browser/EditorRemoveTest.ts @@ -21,7 +21,7 @@ describe('browser.tinymce.core.EditorRemoveTest', () => { assertTextareaDisplayStyle(editor, 'none'); editor.remove(); assertTextareaDisplayStyle(editor, expectedStyle); - await EditorManager.init({ selector: '#tinymce' }); + await EditorManager.init({ selector: '#tinymce', license_key: 'gpl' }); assertTextareaDisplayStyle(editor, expectedStyle); McEditor.remove(editor); }; diff --git a/modules/tinymce/src/core/test/ts/browser/EditorTest.ts b/modules/tinymce/src/core/test/ts/browser/EditorTest.ts index 05921af2f7..d760cb405e 100644 --- a/modules/tinymce/src/core/test/ts/browser/EditorTest.ts +++ b/modules/tinymce/src/core/test/ts/browser/EditorTest.ts @@ -1,9 +1,9 @@ import { UiFinder, Waiter } from '@ephox/agar'; import { context, describe, it } from '@ephox/bedrock-client'; -import { Fun } from '@ephox/katamari'; +import { Fun, Obj } from '@ephox/katamari'; import { PlatformDetection } from '@ephox/sand'; import { Attribute, Class, SugarBody } from '@ephox/sugar'; -import { TinyAssertions, TinyHooks, TinySelections } from '@ephox/wrap-mcagar'; +import { McEditor, TinyAssertions, TinyHooks, TinySelections } from '@ephox/wrap-mcagar'; import { assert } from 'chai'; import Editor from 'tinymce/core/api/Editor'; @@ -15,6 +15,7 @@ import URI from 'tinymce/core/api/util/URI'; import { UndoLevel } from 'tinymce/core/undo/UndoManagerTypes'; import * as HtmlUtils from '../module/test/HtmlUtils'; +import * as UuidUtils from '../module/test/UuidUtils'; describe('browser.tinymce.core.EditorTest', () => { const browser = PlatformDetection.detect().browser; @@ -580,4 +581,25 @@ describe('browser.tinymce.core.EditorTest', () => { checkWithManager('Plugin which has not loaded does not return true', 'Has ParticularPlugin In List', 'ParticularPlugin', false, false); }); }); + + context('editorUid', () => { + it('TINY-12021: should exist and be unique', async () => { + const editor = hook.editor(); + const editor2 = await McEditor.pCreate(); + UuidUtils.assertIsUuid(editor.editorUid); + UuidUtils.assertIsUuid(editor2.editorUid); + assert.notStrictEqual(editor.editorUid, editor2.editorUid); + McEditor.remove(editor2); + }); + + it('TINY-12020: should be locked', () => { + const editor = hook.editor(); + const keys = new Set(Obj.keys(editor)); + assert.isTrue(keys.has('editorUid'), `expected editorUid when enumerating editor`); + assert.throws(() => { + editor.editorUid = 'some_random_value'; + }); + assert.notStrictEqual(editor.editorUid, 'some_random_value'); + }); + }); }); diff --git a/modules/tinymce/src/core/test/ts/browser/init/EditorInitializationTest.ts b/modules/tinymce/src/core/test/ts/browser/init/EditorInitializationTest.ts index 4d1353914d..e777068238 100644 --- a/modules/tinymce/src/core/test/ts/browser/init/EditorInitializationTest.ts +++ b/modules/tinymce/src/core/test/ts/browser/init/EditorInitializationTest.ts @@ -43,6 +43,7 @@ describe('browser.tinymce.core.init.EditorInitializationTest', () => { // eslint-disable-next-line @typescript-eslint/no-floating-promises EditorManager.init({ target: elm1, + license_key: 'gpl', init_instance_callback: (ed) => { assert.strictEqual(ed.targetElm, elm1); done(); @@ -57,6 +58,7 @@ describe('browser.tinymce.core.init.EditorInitializationTest', () => { // eslint-disable-next-line @typescript-eslint/no-floating-promises EditorManager.init({ target: elm, + license_key: 'gpl', init_instance_callback: (ed) => { assert.isAbove(ed.id.length, 0, 'editors id set to: ' + ed.id); assert.strictEqual(ed.targetElm, elm); @@ -73,6 +75,7 @@ describe('browser.tinymce.core.init.EditorInitializationTest', () => { EditorManager.init({ selector: '#elm-2', target: elm1, + license_key: 'gpl', init_instance_callback: (ed) => { assert.strictEqual(ed.targetElm, elm2); done(); @@ -83,6 +86,7 @@ describe('browser.tinymce.core.init.EditorInitializationTest', () => { it('selector on non existing targets', () => { return EditorManager.init({ selector: '#non-existing-id', + license_key: 'gpl', }).then((result) => { assert.lengthOf(result, 0, 'Should be a result that is zero length'); }); @@ -98,6 +102,7 @@ describe('browser.tinymce.core.init.EditorInitializationTest', () => { EditorManager.init({ selector: '.elm-even', target: elm1, + license_key: 'gpl', init_instance_callback: (ed) => { assert.notStrictEqual(ed.targetElm, elm1, 'target option ignored'); assert.notInclude(targets, ed.targetElm); @@ -117,6 +122,7 @@ describe('browser.tinymce.core.init.EditorInitializationTest', () => { base_url: '/compiled/fake/url', suffix: '.min', selector: '#elm-1', + license_key: 'gpl', init_instance_callback: (ed) => { assert.equal(EditorManager.suffix, '.min', 'Should have set suffix on EditorManager'); assert.equal(ed.suffix, '.min', 'Should have set suffix on editor'); @@ -135,6 +141,7 @@ describe('browser.tinymce.core.init.EditorInitializationTest', () => { viewBlock.update('

a

'); await EditorManager.init({ selector: '.tinymce-editor', + license_key: 'gpl', inline: true, promotion: false, toolbar_mode: 'wrap', @@ -161,6 +168,7 @@ describe('browser.tinymce.core.init.EditorInitializationTest', () => { return EditorManager.init({ selector: '.tinymce-editor', + license_key: 'gpl', inline: true, promotion: false, toolbar_mode: 'wrap' @@ -239,6 +247,7 @@ describe('browser.tinymce.core.init.EditorInitializationTest', () => { // eslint-disable-next-line @typescript-eslint/no-floating-promises EditorManager.init({ selector, + license_key: 'gpl', init_instance_callback: (ed) => { assert.equal(ed.getContent({ format: 'text' }), expectedEditorContent, 'Expect editor to have content'); done(); diff --git a/modules/tinymce/src/core/test/ts/browser/init/LicenseKeyTest.ts b/modules/tinymce/src/core/test/ts/browser/init/LicenseKeyTest.ts new file mode 100644 index 0000000000..32a1d8a5ec --- /dev/null +++ b/modules/tinymce/src/core/test/ts/browser/init/LicenseKeyTest.ts @@ -0,0 +1,84 @@ + +import { before, context, describe, it } from '@ephox/bedrock-client'; +import { TinyHooks } from '@ephox/wrap-mcagar'; +import { assert } from 'chai'; + +import Editor from 'tinymce/core/api/Editor'; +import { TinyMCE } from 'tinymce/core/api/Tinymce'; + +declare const tinymce: TinyMCE; + +describe('browser.tinymce.core.init.LicenseKeyTest', () => { + context('Non-GPL License Key Manager', () => { + before(() => { + tinymce._addLicenseKeyManager(() => { + return { + validate: () => Promise.resolve(false) + }; + }); + }); + + context('No license key specified', () => { + const hook = TinyHooks.bddSetupLight({ + license_key: undefined, + base_url: '/project/tinymce/js/tinymce' + }); + + it('TINY-12058: editor.licenseKeyManager should be defined', () => { + const editor = hook.editor(); + assert.isObject(editor.licenseKeyManager); + assert.isFunction(editor.licenseKeyManager.validate); + }); + + it('TINY-12058: validate should return false by default', async () => { + const editor = hook.editor(); + const result = await editor.licenseKeyManager.validate({}); + assert.isFalse(result); + }); + }); + + context('Non-GPL license key specified', () => { + const hook = TinyHooks.bddSetupLight({ + base_url: '/project/tinymce/js/tinymce', + license_key: 'foo' + }, []); + + it('TINY-12058: editor.licenseKeyManager should be defined', () => { + const editor = hook.editor(); + assert.isObject(editor.licenseKeyManager); + assert.isFunction(editor.licenseKeyManager.validate); + }); + + it('TINY-12058: validate should return true by default', async () => { + const editor = hook.editor(); + const result = await editor.licenseKeyManager.validate({}); + assert.isFalse(result); + }); + }); + }); + + context('GPL License Key Manager', () => { + const hook = TinyHooks.bddSetupLight({ + base_url: '/project/tinymce/js/tinymce', + license_key: 'gpl' + }); + + it('TINY-12058: editor.licenseKeyManager should be defined', () => { + const editor = hook.editor(); + assert.isObject(editor.licenseKeyManager); + assert.isFunction(editor.licenseKeyManager.validate); + }); + + it('TINY-12058: validate should return true by default', async () => { + const editor = hook.editor(); + const result = await editor.licenseKeyManager.validate({}); + assert.isTrue(result); + }); + + it('TINY-12058: validate should return false when given any plugin', async () => { + const editor = hook.editor(); + const result = await editor.licenseKeyManager.validate({ plugin: 'foo' }); + assert.isFalse(result); + }); + }); +}); diff --git a/modules/tinymce/src/core/test/ts/browser/init/LicenseKeyValidationTest.ts b/modules/tinymce/src/core/test/ts/browser/init/LicenseKeyValidationTest.ts deleted file mode 100644 index 8391292405..0000000000 --- a/modules/tinymce/src/core/test/ts/browser/init/LicenseKeyValidationTest.ts +++ /dev/null @@ -1,144 +0,0 @@ - -import { after, afterEach, before, context, describe, it } from '@ephox/bedrock-client'; -import { Arr, Fun, Global } from '@ephox/katamari'; -import { TinyHooks } from '@ephox/mcagar'; -import { assert } from 'chai'; - -import Editor from 'tinymce/core/api/Editor'; -import * as LicenseKeyValidation from 'tinymce/core/init/LicenseKeyValidation'; - -describe('browser.tinymce.core.init.LicenseKeyValidationTest', () => { - let oldWarn: typeof console.warn; - let messages: string[] = []; - const expectedLogMessage = `TinyMCE is running in evaluation mode. Provide a valid license key or add license_key: 'gpl' to the init config to agree to the open source license terms. Read more at https://www.tiny.cloud/license-key/`; - const invalidGeneratedKeyToShort = Arr.range(63, Fun.constant('x')).join(''); - const invalidGeneratedKeyToLong = Arr.range(512, Fun.constant('x')).join(''); - const validGeneratedKey = Arr.range(67, Fun.constant('x')).join(''); - - const beforeHandler = () => { - messages = []; - oldWarn = Global.console.warn; - Global.console.warn = (...args: any[]) => { - messages.push(Arr.map(args, (arg) => String(arg)).join(' ')); - }; - }; - - const afterHandler = () => { - Global.console.warn = oldWarn; - }; - - context('LicenseKeyValidator API', () => { - before(beforeHandler); - after(afterHandler); - - const hook = TinyHooks.bddSetupLight({ - base_url: '/project/tinymce/js/tinymce' - }); - - afterEach(() => { - hook.editor().options.unset('license_key'); - }); - - it('validateLicenseKey GPL', () => { - assert.equal(LicenseKeyValidation.validateLicenseKey('GPL'), 'VALID'); - assert.equal(LicenseKeyValidation.validateLicenseKey('gPl'), 'VALID'); - assert.equal(LicenseKeyValidation.validateLicenseKey('gpl'), 'VALID'); - }); - - it('validateLicenseKey generated', () => { - assert.equal(LicenseKeyValidation.validateLicenseKey(invalidGeneratedKeyToShort), 'INVALID', 'To short'); - assert.equal(LicenseKeyValidation.validateLicenseKey(invalidGeneratedKeyToLong), 'INVALID', 'To to long'); - assert.equal(LicenseKeyValidation.validateLicenseKey(validGeneratedKey), 'VALID', 'Is valid'); - }); - - it('validateEditorLicenseKey no license key set', () => { - messages = []; - LicenseKeyValidation.validateEditorLicenseKey(hook.editor()); - assert.deepEqual(messages, [ expectedLogMessage ], 'Should produce a message since license_key is missing'); - }); - - it('validateEditorLicenseKey GPL license key set', () => { - const editor = hook.editor(); - - messages = []; - editor.options.set('license_key', 'gpl'); - LicenseKeyValidation.validateEditorLicenseKey(hook.editor()); - assert.deepEqual(messages, [ ], 'Should not produce a message since GPL is valid'); - }); - - it('validateEditorLicenseKey generated valid license key set', () => { - const editor = hook.editor(); - - messages = []; - editor.options.set('license_key', validGeneratedKey); - LicenseKeyValidation.validateEditorLicenseKey(hook.editor()); - assert.deepEqual(messages, [ ], 'Should not produce a message since generated key is valid'); - }); - - it('validateEditorLicenseKey api_key set but no license_key', () => { - const editor = hook.editor(); - - messages = []; - editor.options.set('api_key', 'some-api-key'); - LicenseKeyValidation.validateEditorLicenseKey(hook.editor()); - assert.deepEqual(messages, [ ], 'Should not produce a message since an api_key was provided'); - editor.options.unset('api_key'); - }); - }); - - context('No license key specified', () => { - before(beforeHandler); - after(afterHandler); - - TinyHooks.bddSetupLight({ - license_key: undefined, - base_url: '/project/tinymce/js/tinymce' - }); - - it('Should have warned while initializing the editor', () => { - assert.deepEqual(messages, [ expectedLogMessage ]); - }); - }); - - context('GPL license key specified', () => { - before(beforeHandler); - after(afterHandler); - - TinyHooks.bddSetupLight({ - base_url: '/project/tinymce/js/tinymce', - license_key: 'gpl' - }); - - it('Should not have any warning messages since gpl was provided', () => { - assert.deepEqual(messages, []); - }); - }); - - context('Invalid license key specified', () => { - before(beforeHandler); - after(afterHandler); - - TinyHooks.bddSetupLight({ - base_url: '/project/tinymce/js/tinymce', - license_key: 'foo' - }, []); - - it('Should have warned while initializing the editor since the key is to short', () => { - assert.deepEqual(messages, [ expectedLogMessage ]); - }); - }); - - context('api_key specified', () => { - before(beforeHandler); - after(afterHandler); - - TinyHooks.bddSetupLight({ - base_url: '/project/tinymce/js/tinymce', - api_key: 'some-api-key' - }); - - it('Should not have any warning messages since an api_key was provided', () => { - assert.deepEqual(messages, []); - }); - }); -}); diff --git a/modules/tinymce/src/core/test/ts/module/test/UuidUtils.ts b/modules/tinymce/src/core/test/ts/module/test/UuidUtils.ts new file mode 100644 index 0000000000..1c137af06d --- /dev/null +++ b/modules/tinymce/src/core/test/ts/module/test/UuidUtils.ts @@ -0,0 +1,12 @@ +import { assert } from 'chai'; + +const assertIsUuid = (uuid: string): void => { + // From https://github.com/uuidjs/uuid/blob/main/src/regex.js + const v4Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + assert.isString(uuid); + assert.match(uuid, v4Regex); +}; + +export { + assertIsUuid +};