diff --git a/packages/devextreme/js/__internal/core/license/license_validation.test.ts b/packages/devextreme/js/__internal/core/license/license_validation.test.ts index daa32510246a..4b10228d005c 100644 --- a/packages/devextreme/js/__internal/core/license/license_validation.test.ts +++ b/packages/devextreme/js/__internal/core/license/license_validation.test.ts @@ -294,9 +294,11 @@ describe('license check', () => { const TOKEN_UNSUPPORTED_VERSION = 'ewogICJmb3JtYXQiOiAyLAogICJjdXN0b21lcklkIjogImIxMTQwYjQ2LWZkZTEtNDFiZC1hMjgwLTRkYjlmOGU3ZDliZCIsCiAgIm1heFZlcnNpb25BbGxvd2VkIjogMjMxCn0=.tTBymZMROsYyMiP6ldXFqGurbzqjhSQIu/pjyEUJA3v/57VgToomYl7FVzBj1asgHpadvysyTUiX3nFvPxbp166L3+LB3Jybw9ueMnwePu5vQOO0krqKLBqRq+TqHKn7k76uYRbkCIo5UajNfzetHhlkin3dJf3x2K/fcwbPW5A='; let trialPanelSpy = jest.spyOn(trialPanel, 'renderTrialPanel'); + let consoleWarnSpy = jest.spyOn(console, 'warn'); beforeEach(() => { jest.spyOn(errors, 'log').mockImplementation(() => {}); + consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); trialPanelSpy = jest.spyOn(trialPanel, 'renderTrialPanel'); setLicenseCheckSkipCondition(false); }); @@ -309,10 +311,10 @@ describe('license check', () => { { token: '', version: '1.0.3' }, { token: null, version: '1.0.4' }, { token: undefined, version: '1.0.50' }, - ])('W0019 error should be logged if license is empty', ({ token, version }) => { + ])('Warning should be logged with no-key message if license is empty', ({ token, version }) => { validateLicense(token as string, version); - expect(errors.log).toHaveBeenCalledTimes(1); - expect(errors.log).toHaveBeenCalledWith('W0019'); + expect(consoleWarnSpy).toHaveBeenCalledTimes(1); + expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('No valid DevExpress license key was found')); }); test.each([ @@ -357,9 +359,10 @@ describe('license check', () => { { token: TOKEN_23_1, version: '12.3.4' }, { token: TOKEN_23_2, version: '23.1.5' }, { token: TOKEN_23_2, version: '23.2.6' }, - ])('No messages should be logged if license is valid', ({ token, version }) => { + ])('Old format license should trigger old-key warning', ({ token, version }) => { validateLicense(token, version); - expect(errors.log).not.toHaveBeenCalled(); + expect(consoleWarnSpy).toHaveBeenCalledTimes(1); + expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('invalid/old DevExtreme key')); }); test.each([ @@ -367,9 +370,9 @@ describe('license check', () => { { token: TOKEN_23_1, version: '12.3.4' }, { token: TOKEN_23_2, version: '23.1.5' }, { token: TOKEN_23_2, version: '23.2.6' }, - ])('Trial panel should not be displayed if license is valid', ({ token, version }) => { + ])('Trial panel should be displayed for old format license keys', ({ token, version }) => { validateLicense(token, version); - expect(trialPanelSpy).not.toHaveBeenCalled(); + expect(trialPanelSpy).toHaveBeenCalledTimes(1); }); test('Trial panel "Buy Now" link must use the jQuery link if no config has been set', () => { @@ -399,15 +402,16 @@ describe('license check', () => { setLicenseCheckSkipCondition(); validateLicense('', '1.0'); expect(errors.log).not.toHaveBeenCalled(); + expect(consoleWarnSpy).not.toHaveBeenCalled(); }); test.each([ { token: TOKEN_23_1, version: '23.2.3' }, { token: TOKEN_23_2, version: '42.4.5' }, - ])('W0020 error should be logged if license is outdated', ({ token, version }) => { + ])('Old format license should trigger old-key warning when outdated', ({ token, version }) => { validateLicense(token, version); - expect(errors.log).toHaveBeenCalledTimes(1); - expect(errors.log).toHaveBeenCalledWith('W0020'); + expect(consoleWarnSpy).toHaveBeenCalledTimes(1); + expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('invalid/old DevExtreme key')); }); test.each([ @@ -425,9 +429,9 @@ describe('license check', () => { { token: TOKEN_23_1, version: '23.2.3-alpha' }, { token: TOKEN_23_2, version: '24.1.0' }, { token: TOKEN_23_2, version: '24.1.abc' }, - ])('Trial panel should not be displayed in previews if the license is for the previous RTM', ({ token, version }) => { + ])('Trial panel should be displayed in previews for old format license keys', ({ token, version }) => { validateLicense(token, version); - expect(trialPanelSpy).not.toHaveBeenCalled(); + expect(trialPanelSpy).toHaveBeenCalledTimes(1); }); test.each([ @@ -440,9 +444,10 @@ describe('license check', () => { { token: TOKEN_UNSUPPORTED_VERSION, version: '1.2.3' }, { token: 'str@nge in.put', version: '1.2.3' }, { token: '3.2.1', version: '1.2.3' }, - ])('W0021 error should be logged if license is corrupted/invalid [%#]', ({ token, version }) => { + ])('License verification warning should be logged if license is corrupted/invalid [%#]', ({ token, version }) => { validateLicense(token, version); - expect(errors.log).toHaveBeenCalledWith('W0021'); + expect(consoleWarnSpy).toHaveBeenCalledTimes(1); + expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('License key verification has failed')); }); test.each([ @@ -514,9 +519,11 @@ describe('internal license check', () => { describe('DevExpress license check', () => { let trialPanelSpy = jest.spyOn(trialPanel, 'renderTrialPanel'); + let consoleWarnSpy = jest.spyOn(console, 'warn'); beforeEach(() => { jest.spyOn(errors, 'log').mockImplementation(() => {}); + consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); trialPanelSpy = jest.spyOn(trialPanel, 'renderTrialPanel'); setLicenseCheckSkipCondition(false); }); @@ -528,14 +535,16 @@ describe('DevExpress license check', () => { test('DevExpress License Key copied from Download Manager (incorrect)', () => { const token = 'LCXv1therestofthekey'; validateLicense(token, '25.1.3'); - expect(errors.log).toHaveBeenCalled(); + expect(consoleWarnSpy).toHaveBeenCalledTimes(1); + expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('.NET license key (LCX)')); expect(trialPanelSpy).toHaveBeenCalled(); }); test('DevExpress License Key generated from LCX key (incorrect)', () => { const token = 'LCPtherestofthekey'; validateLicense(token, '25.1.3'); - expect(errors.log).toHaveBeenCalled(); + expect(consoleWarnSpy).toHaveBeenCalledTimes(1); + expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('License key verification has failed')); expect(trialPanelSpy).toHaveBeenCalled(); }); }); diff --git a/packages/devextreme/js/__internal/core/license/license_validation.ts b/packages/devextreme/js/__internal/core/license/license_validation.ts index ed8540df4ab9..746ffa3e5002 100644 --- a/packages/devextreme/js/__internal/core/license/license_validation.ts +++ b/packages/devextreme/js/__internal/core/license/license_validation.ts @@ -14,6 +14,7 @@ import { } from './const'; import { INTERNAL_USAGE_ID, PUBLIC_KEY } from './key'; import { isProductOnlyLicense, parseDevExpressProductKey } from './lcp_key_validation/lcp_key_validator'; +import { logLicenseWarning } from './license_warnings'; import { pad } from './pkcs1'; import { compareSignatures } from './rsa_bigint'; import { sha1 } from './sha1'; @@ -156,17 +157,21 @@ function getLicenseCheckParams({ const { major, minor } = preview ? getPreviousMajorVersion(version) : version; if (!licenseKey) { - return { preview, error: 'W0019' }; + return { preview, error: 'W0019', warningType: 'no-key' }; } if (hasLicensePrefix(licenseKey, 'LCX')) { - return { preview, error: 'W0024' }; + return { preview, error: 'W0021', warningType: 'lcx-used' }; } + const isProductKey = isProductOnlyLicense(licenseKey); const license = parseLicenseKey(licenseKey); if (license.kind === TokenKind.corrupted) { - return { preview, error: 'W0021' }; + if (license.error === 'product-kind') { + return { preview, error: 'W0021', warningType: 'no-devextreme-license' }; + } + return { preview, error: 'W0021', warningType: 'invalid-key' }; } if (license.kind === TokenKind.internal) { @@ -174,16 +179,25 @@ function getLicenseCheckParams({ } if (!(major && minor)) { - return { preview, error: 'W0021' }; + return { preview, error: 'W0021', warningType: 'invalid-key' }; + } + + if (!isProductKey) { + return { preview, error: 'W0021', warningType: 'old-devextreme-key' }; } if (major * 10 + minor > license.payload.maxVersionAllowed) { - return { preview, error: 'W0020' }; + return { + preview, + error: 'W0020', + warningType: 'version-mismatch', + maxVersionAllowed: license.payload.maxVersionAllowed, + }; } return { preview, error: undefined }; } catch { - return { preview, error: 'W0021' }; + return { preview, error: 'W0021', warningType: 'invalid-key' }; } } @@ -193,16 +207,13 @@ export function validateLicense(licenseKey: string, versionStr: string = fullVer } validationPerformed = true; - if (isUnsupportedKeyFormat(licenseKey)) { - displayTrialPanel(); - return; - } - const version = parseVersion(versionStr); const versionsCompatible = assertedVersionsCompatible(version); - const { internal, error } = getLicenseCheckParams({ + const { + internal, error, warningType, maxVersionAllowed, + } = getLicenseCheckParams({ licenseKey, version, }); @@ -218,7 +229,19 @@ export function validateLicense(licenseKey: string, versionStr: string = fullVer const preview = isPreview(version.patch); if (error) { - errors.log(preview ? 'W0022' : error); + if (preview) { + errors.log('W0022'); + } else if (warningType) { + const versionInfo = warningType === 'version-mismatch' && maxVersionAllowed !== undefined + ? { + keyVersion: `${Math.floor(maxVersionAllowed / 10)}.${maxVersionAllowed % 10}`, + requiredVersion: `${version.major}.${version.minor}`, + } + : undefined; + logLicenseWarning(warningType, versionStr, versionInfo); + } else { + errors.log(error); + } return; } diff --git a/packages/devextreme/js/__internal/core/license/license_warnings.ts b/packages/devextreme/js/__internal/core/license/license_warnings.ts new file mode 100644 index 000000000000..c4b73072b225 --- /dev/null +++ b/packages/devextreme/js/__internal/core/license/license_warnings.ts @@ -0,0 +1,88 @@ +import type { LicenseWarningType } from './types'; + +const SOURCE = '[devextreme-license]'; + +export const TEMPLATES = Object.freeze({ + warningPrefix: (code: string | number): string => `Warning number: ${code}. For evaluation purposes only. Redistribution prohibited.`, + + keyNotFound: 'No valid DevExpress license key was found on this machine.', + + keyWasFound: (type: string, path?: string): string => { + switch (type) { + case 'envVariable': + return 'The DevExpress license key was retrieved from the "DevExpress_License" environment variable.'; + case 'envPath': + return 'The DevExpress license key was retrieved from the "DevExpress_LicensePath" environment variable.'; + case 'file': + return `The DevExpress license key was retrieved from file: "${path}".`; + default: + return 'The DevExpress license key was retrieved.'; + } + }, + + keyVerificationFailed: (type?: string, keyVersion?: string, requiredVersion?: string): string => { + switch (type) { + case 'incompatibleVersion': + return `Incompatible DevExpress license key version (${keyVersion}). Download and register an updated DevExpress license key (${requiredVersion}+). Clear npm/IDE/NuGet cache and rebuild your project.`; + default: + return 'License key verification has failed.'; + } + }, + + purchaseLicense: (version: string): string => `Purchase a license to continue use of DevExtreme (v${version}). Included in subscriptions: Universal, DXperience, ASP.NET and Blazor, DevExtreme Complete. To purchase a license, visit https://js.devexpress.com/Buy/`, + + installationInstructions: 'If you own a licensed/registered version or if you are using a 30-day trial version of DevExpress product libraries on a development machine, download your personal license key and verify it with the devextreme-license tools. Setup instructions: https://js.devexpress.com/Documentation/Guide/Common/Licensing', + + // eslint-disable-next-line spellcheck/spell-checker + lcxUsedInsteadOfLcp: 'The .NET license key (LCX) is used instead of the DevExpress license key (LCP).', + + oldDevExtremeKey: 'The invalid/old DevExtreme key is used instead of the DevExpress license key.', + + licenseId: (id: string): string => `License ID: ${id}`, +}); + +export function logLicenseWarning( + warningType: LicenseWarningType, + version: string, + versionInfo?: { keyVersion: string; requiredVersion: string }, +): void { + const T = TEMPLATES; + + const purchaseLine = `${SOURCE} ${T.warningPrefix('W0019')} ${T.purchaseLicense(version)}`; + const installLine = `${SOURCE} ${T.warningPrefix('W0021')} ${T.installationInstructions}`; + + const lines: string[] = [purchaseLine]; + + switch (warningType) { + case 'no-key': + lines.push('', T.keyNotFound, '', installLine); + break; + + case 'invalid-key': + lines.push('', T.keyVerificationFailed(), '', installLine); + break; + + case 'lcx-used': + // eslint-disable-next-line spellcheck/spell-checker + lines.push('', T.keyVerificationFailed(), T.lcxUsedInsteadOfLcp, '', installLine); + break; + + case 'old-devextreme-key': + lines.push('', T.keyVerificationFailed(), T.oldDevExtremeKey, '', installLine); + break; + + case 'version-mismatch': { + const incompatibleLine = `${SOURCE} ${T.warningPrefix('W0020')} ${T.keyVerificationFailed('incompatibleVersion', versionInfo?.keyVersion, versionInfo?.requiredVersion)}`; + lines.push('', T.keyVerificationFailed(), '', incompatibleLine); + break; + } + + case 'no-devextreme-license': + // Only the purchase line, no additional details + break; + default: + break; + } + + console.warn(lines.join('\n')); +} diff --git a/packages/devextreme/js/__internal/core/license/types.ts b/packages/devextreme/js/__internal/core/license/types.ts index 4e38ccd812ee..104340d3584f 100644 --- a/packages/devextreme/js/__internal/core/license/types.ts +++ b/packages/devextreme/js/__internal/core/license/types.ts @@ -37,8 +37,17 @@ export const PAYLOAD_ERROR: ErrorToken = { kind: TokenKind.corrupted, error: 'pa export const VERSION_ERROR: ErrorToken = { kind: TokenKind.corrupted, error: 'version' }; export const PRODUCT_KIND_ERROR: ErrorToken = { kind: TokenKind.corrupted, error: 'product-kind' }; +export type LicenseWarningType = 'no-key' + | 'invalid-key' + | 'lcx-used' + | 'old-devextreme-key' + | 'version-mismatch' + | 'no-devextreme-license'; + export interface LicenseCheckParams { preview: boolean; internal?: true; error: LicenseVerifyResult | undefined; + warningType?: LicenseWarningType; + maxVersionAllowed?: number; } diff --git a/packages/devextreme/license/devextreme-license-plugin.js b/packages/devextreme/license/devextreme-license-plugin.js index 7777037fb422..f84ecbb7711f 100644 --- a/packages/devextreme/license/devextreme-license-plugin.js +++ b/packages/devextreme/license/devextreme-license-plugin.js @@ -1,6 +1,6 @@ const { createUnplugin } = require('unplugin'); const { getDevExpressLCXKey } = require('./dx-get-lcx'); -const { tryConvertLCXtoLCP, getLCPWarning } = require('./dx-lcx-2-lcp'); +const { tryConvertLCXtoLCP, getLCPInfo } = require('./dx-lcx-2-lcp'); const { MESSAGES } = require('./messages'); const PLUGIN_NAME = 'devextreme-bundler-plugin'; @@ -28,10 +28,13 @@ const DevExtremeLicensePlugin = createUnplugin(() => { warn(ctx, msg); } - function warnLicenseIssue(ctx, source, warning) { + function warnLicenseIssue(ctx, source, licenseId, warning) { try { if(ctx && typeof ctx.warn === 'function') { ctx.warn(`${PLUGIN_PREFIX} DevExpress license key (LCX) retrieved from: ${source}`); + if(licenseId) { + ctx.warn(`${PLUGIN_PREFIX} License ID: ${licenseId}`); + } ctx.warn(`${PLUGIN_PREFIX} Warning: ${warning}`); } } catch{} @@ -51,13 +54,13 @@ const DevExtremeLicensePlugin = createUnplugin(() => { const lcp = tryConvertLCXtoLCP(lcx); if(!lcp) { - warnLicenseIssue(ctx, source, MESSAGES.keyNotFound); + warnLicenseIssue(ctx, source, null, MESSAGES.keyNotFound); return (lcpCache = null); } - const warning = getLCPWarning(lcp); + const { warning, licenseId } = getLCPInfo(lcp); if(warning) { - warnLicenseIssue(ctx, source, warning); + warnLicenseIssue(ctx, source, licenseId, warning); } return (lcpCache = lcp); diff --git a/packages/devextreme/license/devextreme-license.js b/packages/devextreme/license/devextreme-license.js index face0c2824e3..759e8dd515d9 100644 --- a/packages/devextreme/license/devextreme-license.js +++ b/packages/devextreme/license/devextreme-license.js @@ -5,11 +5,12 @@ const fs = require('fs'); const path = require('path'); const { getDevExpressLCXKey } = require('./dx-get-lcx'); -const { convertLCXtoLCP, getLCPWarning } = require('./dx-lcx-2-lcp'); -const { MESSAGES } = require('./messages'); +const { tryConvertLCXtoLCP, getLCPInfo } = require('./dx-lcx-2-lcp'); +const { TEMPLATES } = require('./messages'); const EXPORT_NAME = 'licenseKey'; const TRIAL_VALUE = 'TRIAL'; +const CLI_PREFIX = '[devextreme-license]'; function fail(msg) { process.stderr.write(msg.endsWith('\n') ? msg : msg + '\n'); @@ -153,24 +154,36 @@ function main() { process.exit(0); } - const { key: lcx, source } = getDevExpressLCXKey() || {}; + const { key: lcx, source, currentVersion } = getDevExpressLCXKey() || {}; let lcp = TRIAL_VALUE; + let licenseId = null; if(lcx) { - try { - lcp = convertLCXtoLCP(lcx); - const warning = getLCPWarning(lcp); - if(warning) { - process.stderr.write(`DevExpress license key (LCX) retrieved from: ${source}\n`); - process.stderr.write(`[devextreme-license] Warning: ${warning}\n`); + lcp = tryConvertLCXtoLCP(lcx) || TRIAL_VALUE; + const { warning, licenseId: id } = getLCPInfo(lcp); + licenseId = id; + if(warning) { + if(licenseId) { + process.stderr.write(`${CLI_PREFIX} ${TEMPLATES.licenseId(licenseId)}\n\n`); } - } catch{ - process.stderr.write(`DevExpress license key (LCX) retrieved from: ${source}\n`); - process.stderr.write(`[devextreme-license] Warning: ${MESSAGES.keyNotFound}\n`); + process.stderr.write(` + ${CLI_PREFIX} ${TEMPLATES.warningPrefix(1000)} ${TEMPLATES.purchaseLicense(currentVersion)}\n\n + ${TEMPLATES.keyWasFound(source.type, source.path)}\n + `); + if(warning.type !== 'trial') { + process.stderr.write(` + ${TEMPLATES.keyVerificationFailed(warning.type, warning.keyVersion, warning.currentVersion)}\n\n + ${CLI_PREFIX} ${TEMPLATES.warningPrefix(TEMPLATES.warningCodeByType(warning.type))} ${TEMPLATES.installationInstructions} + `); + } } } else { - process.stderr.write(`[devextreme-license] Warning: ${MESSAGES.keyNotFound}\n`); + process.stderr.write(` + ${CLI_PREFIX} ${TEMPLATES.warningPrefix(1000)} ${TEMPLATES.purchaseLicense(currentVersion)}\n\n + ${TEMPLATES.keyNotFound}\n\n + ${CLI_PREFIX} ${TEMPLATES.warningPrefix(1001)} ${TEMPLATES.installationInstructions} + `); } if(!opts.outPath) { diff --git a/packages/devextreme/license/dx-get-lcx.js b/packages/devextreme/license/dx-get-lcx.js index f8535e084e18..570895982abd 100644 --- a/packages/devextreme/license/dx-get-lcx.js +++ b/packages/devextreme/license/dx-get-lcx.js @@ -77,26 +77,52 @@ function resolveFromLicensePathEnv(licensePathValue) { return path.join(p, LICENSE_FILE); } +function readDevExtremeVersion() { + try { + const pkgPath = require('path').join(__dirname, '..', 'package.json'); + const pkg = JSON.parse(require('fs').readFileSync(pkgPath, 'utf8')); + const parts = String(pkg.version || '').split('.'); + const major = parseInt(parts[0], 10); + const minor = parseInt(parts[1], 10); + if(!isNaN(major) && !isNaN(minor)) { + return { major, minor, code: major * 10 + minor }; + } + } catch{} + return null; +} + +function buildVersionString(devExtremeVersion){ + const { major, minor, code: currentCode } = devExtremeVersion; + return `${major}.${minor}`; +} + function getDevExpressLCXKey() { + const devExtremeVersion = readDevExtremeVersion(); + let currentVersion = ''; + if(devExtremeVersion) { + currentVersion = buildVersionString(devExtremeVersion); + } if(hasEnvVar(LICENSE_ENV)) { - return { key: normalizeKey(process.env[LICENSE_ENV]), source: `env:${LICENSE_ENV}` }; + return { key: normalizeKey(process.env[LICENSE_ENV]), source: { type: 'envVariable' }, currentVersion }; } if(hasEnvVar(LICENSE_PATH_ENV)) { const licensePath = resolveFromLicensePathEnv(process.env[LICENSE_PATH_ENV]); const key = normalizeKey(readTextFileIfExists(licensePath)); - return { key, source: key ? `file:${licensePath}` : `env:${LICENSE_PATH_ENV}` }; + return { key, source: { type: 'envPath' }, currentVersion }; } const defaultPath = getDefaultLicenseFilePath(); const fromDefault = normalizeKey(readTextFileIfExists(defaultPath)); if(fromDefault) { - return { key: fromDefault, source: `file:${defaultPath}` }; + return { key: fromDefault, source: { type: 'file', path: defaultPath }, currentVersion }; } - return { key: null, source: null }; + return { key: null, source: null, currentVersion }; } module.exports = { getDevExpressLCXKey, + readDevExtremeVersion, + buildVersionString }; diff --git a/packages/devextreme/license/dx-lcx-2-lcp.js b/packages/devextreme/license/dx-lcx-2-lcp.js index 342dfaeca67d..1e863481e20f 100644 --- a/packages/devextreme/license/dx-lcx-2-lcp.js +++ b/packages/devextreme/license/dx-lcx-2-lcp.js @@ -1,5 +1,4 @@ - const { MESSAGES } = require('./messages'); const LCX_SIGNATURE = 'LCXv1'; const LCP_SIGNATURE = 'LCPv1'; @@ -133,20 +132,40 @@ const GENERAL_ERROR = { kind: TokenKind.corrupted, error: 'general' }; const DESERIALIZATION_ERROR = { kind: TokenKind.corrupted, error: 'deserialization' }; const PRODUCT_KIND_ERROR = { kind: TokenKind.corrupted, error: 'product-kind' }; +function readDevExtremeVersion() { + try { + const pkgPath = require('path').join(__dirname, '..', 'package.json'); + const pkg = JSON.parse(require('fs').readFileSync(pkgPath, 'utf8')); + const parts = String(pkg.version || '').split('.'); + const major = parseInt(parts[0], 10); + const minor = parseInt(parts[1], 10); + if(!isNaN(major) && !isNaN(minor)) { + return { major, minor, code: major * 10 + minor }; + } + } catch{} + return null; +} + +function buildVersionString(devExtremeVersion){ + const { major, minor, code: currentCode } = devExtremeVersion; + return `${major}.${minor}`; +} + function productsFromString(encodedString) { if(!encodedString) { return { products: [], errorToken: GENERAL_ERROR }; } try { - const productTuples = encodedString.split(';').slice(1).filter(e => e.length > 0); + const splitInfo = encodedString.split(';'); + const licenseId = splitInfo[0]; + const productTuples = splitInfo.slice(1).filter((entry) => entry.length > 0); const products = productTuples.map(tuple => { const parts = tuple.split(','); - return { - version: Number.parseInt(parts[0], 10), - products: BigInt(parts[1]), - }; + const version = Number.parseInt(parts[0], 10); + const products = BigInt(parts[1]); + return { version, products }; }); - return { products }; + return { products, licenseId }; } catch{ return { products: [], errorToken: DESERIALIZATION_ERROR }; } @@ -174,7 +193,7 @@ function parseLCP(lcpString) { const productsPayload = decoded.slice(SIGN_LENGTH); const decodedPayload = mapString(productsPayload, DECODE_MAP); - const { products, errorToken } = productsFromString(decodedPayload); + const { products, errorToken, licenseId } = productsFromString(decodedPayload); if(errorToken) { return errorToken; } @@ -186,7 +205,7 @@ function parseLCP(lcpString) { return { kind: TokenKind.verified, - payload: { customerId: '', maxVersionAllowed }, + payload: { customerId: '', maxVersionAllowed, licenseId }, }; } catch{ return GENERAL_ERROR; @@ -197,50 +216,53 @@ function formatVersionCode(versionCode) { return `v${Math.floor(versionCode / 10)}.${versionCode % 10}`; } -function readDevExtremeVersion() { - try { - const pkgPath = require('path').join(__dirname, '..', 'package.json'); - const pkg = JSON.parse(require('fs').readFileSync(pkgPath, 'utf8')); - const parts = String(pkg.version || '').split('.'); - const major = parseInt(parts[0], 10); - const minor = parseInt(parts[1], 10); - if(!isNaN(major) && !isNaN(minor)) { - return { major, minor, code: major * 10 + minor }; - } - } catch{} - return null; -} - -function getLCPWarning(lcpString) { +function getLCPInfo(lcpString) { const token = parseLCP(lcpString); + let warning = null; + let licenseId = null; + let currentVersion = ''; if(token.kind === TokenKind.corrupted) { - if(token.error === 'product-kind') { - return MESSAGES.trial; + switch(token.error) { + case 'general': + warning = { type: 'general' }; + break; + case 'deserialization': + warning = { type: 'corrupted' }; + break; + case 'product-kind': + warning = { type: 'trial' }; + break; } - return null; - } - - // token.kind === TokenKind.verified — check version compatibility - const devExtremeVersion = readDevExtremeVersion(); - if(devExtremeVersion) { - const { major, minor, code: currentCode } = devExtremeVersion; - const { maxVersionAllowed } = token.payload; - if(maxVersionAllowed < currentCode) { - return MESSAGES.versionIncompatible( - formatVersionCode(maxVersionAllowed), - `v${major}.${minor}`, - ); + } else { + // token.kind === TokenKind.verified — check version compatibility + licenseId = token.payload.licenseId || null; + const devExtremeVersion = readDevExtremeVersion(); + if(devExtremeVersion) { + currentVersion = buildVersionString(devExtremeVersion); + const { maxVersionAllowed } = token.payload; + if(maxVersionAllowed < devExtremeVersion.code) { + warning = { + type:'incompatibleVersion', + keyVersion: formatVersionCode(maxVersionAllowed), + currentVersion + }; + } } } - return null; + return { warning, licenseId, currentVersion }; +} + +function getLCPWarning(lcpString) { + return getLCPInfo(lcpString).warning; } module.exports = { convertLCXtoLCP, tryConvertLCXtoLCP, parseLCP, + getLCPInfo, getLCPWarning, TokenKind, LCX_SIGNATURE, diff --git a/packages/devextreme/license/messages.js b/packages/devextreme/license/messages.js index e110da4e5d17..b619c72838fb 100644 --- a/packages/devextreme/license/messages.js +++ b/packages/devextreme/license/messages.js @@ -13,14 +13,48 @@ const MESSAGES = Object.freeze({ 'Please purchase a license to continue use of the following DevExpress product libraries: ' + 'Universal, DXperience, ASP.NET and Blazor, DevExtreme Complete.', - versionIncompatible: (keyVersion, requiredVersion) => - 'For evaluation purposes only. Redistribution prohibited. ' + - `Incompatible DevExpress license key version (${keyVersion}). ` + - `Download and register an updated DevExpress license key (${requiredVersion}+). ` + - 'Clear IDE/NuGet cache and rebuild your project (devexpress.com/DX1002).', - resolveFailed: 'Failed to resolve license key. Placeholder will remain.', }); -module.exports = { MESSAGES }; +const TEMPLATES = Object.freeze({ + warningPrefix: (number) => `Warning number: DX${number}. For evaluation purposes only. Redistribution prohibited.`, + keyNotFound: 'No valid DevExpress license key was found on this machine.', + keyWasFound: (type, path) => { + switch(type) { + case 'envVariable': + return 'The DevExpress license key was retrieved from the "DevExpress_License" environment variable.'; + case 'envPath': + return 'The DevExpress license key was retrieved from the "DevExpress_LicensePath" environment variable.'; + case 'file': + return `The DevExpress license key was retrieved from file: "${path}".`; + default: + return 'The DevExpress license key was retrieved.'; + } + }, + keyVerificationFailed: (type, keyVersion, requiredVersion) => { + switch(type) { + case 'incompatibleVersion': + return `Incompatible DevExpress license key version (${keyVersion}). Download and register an updated DevExpress license key (${requiredVersion}+). Clear npm/IDE/NuGet cache and rebuild your project.`; + default: + return 'License key verification has failed.'; + } + }, + warningCodeByType: (type) => { + switch(type) { + case 'general': + return 1001; + case 'incompatibleVersion': + return 1002; + default: + return 1001; + } + }, + purchaseLicense: (version) => + `Purchase a license to continue use of DevExtreme (v${version}). Included in subscriptions: Universal, DXperience, ASP.NET and Blazor, DevExtreme Complete. To purchase a license, visit https://js.devexpress.com/Buy/`, + installationInstructions: 'If you own a licensed/registered version or if you are using a 30-day trial version of DevExpress product libraries on a development machine, download your personal license key and verify it with the devextreme-license tools. Setup instructions: https://js.devexpress.com/Documentation/Guide/Common/Licensing', + oldDevExtremeKey: 'The invalid/old DevExtreme key is used instead of the DevExpress license key.', + licenseId: (id) => `License ID: ${id}`, +}); + +module.exports = { MESSAGES, TEMPLATES };