Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Expand All @@ -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([
Expand Down Expand Up @@ -357,19 +359,20 @@ 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([
{ token: TOKEN_23_1, version: '23.1.3' },
{ 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', () => {
Expand Down Expand Up @@ -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([
Expand All @@ -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([
Expand All @@ -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([
Expand Down Expand Up @@ -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);
});
Expand All @@ -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();
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -156,34 +157,47 @@ 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) {
return { preview, internal: true, error: license.internalUsageId === INTERNAL_USAGE_ID ? undefined : 'W0020' };
}

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

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

Expand Down
88 changes: 88 additions & 0 deletions packages/devextreme/js/__internal/core/license/license_warnings.ts
Original file line number Diff line number Diff line change
@@ -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'));
}
9 changes: 9 additions & 0 deletions packages/devextreme/js/__internal/core/license/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
13 changes: 8 additions & 5 deletions packages/devextreme/license/devextreme-license-plugin.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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{}
Expand All @@ -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);
Expand Down
Loading
Loading