TINY-12001: Add TinyMCE 8 license key implementation (#10421)

This commit is contained in:
ltrouton
2025-06-27 14:13:40 +10:00
committed by GitHub
parent 7cddff9795
commit 8e31881d42
24 changed files with 507 additions and 189 deletions

View File

@ -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

View File

@ -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

View File

@ -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
};

View File

@ -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
};

View File

@ -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');
});
});
});

View File

@ -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);

View File

@ -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,

View File

@ -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<NativeEventMap> | 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);

View File

@ -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<EditorManagerEventMap> {
documentBaseURL: string;
i18n: I18n;
suffix: string;
pageUid: string;
add (this: EditorManager, editor: Editor): Editor;
addI18n: (code: string, item: Record<string, string>) => void;
@ -118,6 +120,7 @@ interface EditorManager extends Observable<EditorManagerEventMap> {
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();

View File

@ -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<void> => {
initTooltipClosing(editor);
initTheme(editor);
initModel(editor);
initLicenseKeyManager(editor);
initPlugins(editor);
const renderInfo = await renderThemeUi(editor);
augmentEditorUiApi(editor, Optional.from(renderInfo.api).getOr({}));

View File

@ -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

View File

@ -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<LicenseKeyManager>;
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<boolean>;
}
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<LicenseKeyManager>();
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;

View File

@ -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/`);
}
};

View File

@ -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<UrlMeta> => 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);

View File

@ -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,

View File

@ -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('<textarea id="ed_1" class="tinymce"></textarea>');
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('<textarea id="ed_1" class="tinymce"></textarea>');
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('<textarea id="ed_1" class="tinymce"></textarea><textarea id="ed_2" class="tinymce"></textarea>');
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);

View File

@ -33,8 +33,10 @@ describe('browser.tinymce.core.EditorManagerSetupTest', () => {
it('script baseURL and suffix with script in svg', () => {
viewBlock.update('<svg><script>!function(){}();</script></svg><script src="http://localhost/nonexistant/tinymce.min.js" type="application/javascript"></script>');
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');
});
});

View File

@ -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('<textarea class="tinymce"></textarea><textarea class="tinymce"></textarea>');
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<string>(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');
}
});
});

View File

@ -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);
};

View File

@ -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<Editor>();
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');
});
});
});

View File

@ -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('<div class="tinymce-editor"><p>a</p></div>');
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();

View File

@ -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<Editor>({
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<Editor>({
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<Editor>({
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);
});
});
});

View File

@ -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<Editor>({
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<Editor>({
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<Editor>({
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<Editor>({
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<Editor>({
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, []);
});
});
});

View File

@ -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
};