diff --git a/CHANGELOG.md b/CHANGELOG.md index a718767a..36913aac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ Change Log +v5.7.0 +--- +* **New option:** `advertisement` allows to control the display of the JavaScript Obfuscator Pro advertisement message in the console. Fixed https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1448 + v5.6.0 --- * Pro API: `obfuscatePro` now fall back to the basic local obfuscation API when no Pro feature (`vmObfuscation` or `parseHtml`) is enabled, instead of throwing an `ApiError` diff --git a/README.md b/README.md index 6191971a..acee6ac8 100644 --- a/README.md +++ b/README.md @@ -555,6 +555,7 @@ Following options are available for the JS Obfuscator: ```javascript { + advertisement: true, compact: true, controlFlowFlattening: false, controlFlowFlatteningThreshold: 0.75, @@ -618,6 +619,7 @@ Following options are available for the JS Obfuscator: -o, --output + --advertisement --compact --config --control-flow-flattening @@ -715,6 +717,13 @@ Following options are available for the JS Obfuscator: +### `advertisement` +Type: `boolean` Default: `true` + +Allows to control the display of the JavaScript Obfuscator Pro advertisement message in the console. + +The message is only shown when using the Node.js CLI in an interactive (TTY) terminal, is never shown in CI environments or in the browser, and is limited to a few displays. Set this option to `false` to disable the advertisement message completely. + ### `compact` Type: `boolean` Default: `true` diff --git a/package.json b/package.json index 1f93fa5a..cacb5db9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "javascript-obfuscator", - "version": "5.6.0", + "version": "5.7.0", "description": "JavaScript obfuscator", "keywords": [ "obfuscator", diff --git a/src/JavaScriptObfuscator.ts b/src/JavaScriptObfuscator.ts index 53381b97..6d8916ad 100644 --- a/src/JavaScriptObfuscator.ts +++ b/src/JavaScriptObfuscator.ts @@ -158,7 +158,7 @@ export class JavaScriptObfuscator implements IJavaScriptObfuscator { * @returns {IObfuscationResult} */ public obfuscate(sourceCode: string): IObfuscationResult { - if (AdvertisementUtils.shouldShowAdvertisement()) { + if (AdvertisementUtils.shouldShowAdvertisement(this.options.advertisement)) { this.logger.advertise(LoggingMessage.JavaScriptObfuscatorProAdFirstPart); this.logger.advertise(LoggingMessage.JavaScriptObfuscatorProAdSecondPart); } diff --git a/src/cli/JavaScriptObfuscatorCLI.ts b/src/cli/JavaScriptObfuscatorCLI.ts index a8283563..08747a88 100644 --- a/src/cli/JavaScriptObfuscatorCLI.ts +++ b/src/cli/JavaScriptObfuscatorCLI.ts @@ -193,6 +193,11 @@ export class JavaScriptObfuscatorCLI implements IInitializable { .usage(' [options]') .version(Utils.buildVersionMessage(process.env.VERSION, process.env.BUILD_TIMESTAMP), '-v, --version') .option('-o, --output ', 'Output path for obfuscated code') + .option( + '--advertisement ', + 'Allows to control the JavaScript Obfuscator Pro advertisement message shown in the console', + BooleanSanitizer + ) .option('--compact ', 'Disable one line output code compacting', BooleanSanitizer) .option('--config ', 'Name of js / json config file') .option('--control-flow-flattening ', 'Enables control flow flattening', BooleanSanitizer) diff --git a/src/custom-code-helpers/CustomCodeHelperObfuscator.ts b/src/custom-code-helpers/CustomCodeHelperObfuscator.ts index 76cc9a36..4755d70d 100644 --- a/src/custom-code-helpers/CustomCodeHelperObfuscator.ts +++ b/src/custom-code-helpers/CustomCodeHelperObfuscator.ts @@ -48,7 +48,8 @@ export class CustomCodeHelperObfuscator implements ICustomCodeHelperObfuscator { numbersToExpressions: this.options.numbersToExpressions, simplify: this.options.simplify, seed: this.randomGenerator.getRawSeed(), - ...additionalOptions + ...additionalOptions, + advertisement: false }).getObfuscatedCode(); } } diff --git a/src/interfaces/options/IOptions.ts b/src/interfaces/options/IOptions.ts index 01890971..9cf31d56 100644 --- a/src/interfaces/options/IOptions.ts +++ b/src/interfaces/options/IOptions.ts @@ -12,6 +12,7 @@ import { SourceMapMode } from '../../enums/source-map/SourceMapMode'; import { SourceMapSourcesMode } from '../../enums/source-map/SourceMapSourcesMode'; export interface IOptions { + readonly advertisement: boolean; readonly compact: boolean; readonly controlFlowFlattening: boolean; readonly controlFlowFlatteningThreshold: number; diff --git a/src/options/Options.ts b/src/options/Options.ts index fc1d92d5..395726c3 100644 --- a/src/options/Options.ts +++ b/src/options/Options.ts @@ -73,6 +73,12 @@ export class Options implements IOptions { } }; + /** + * @type {boolean} + */ + @IsBoolean() + public readonly advertisement!: boolean; + /** * @type {boolean} */ diff --git a/src/options/presets/Default.ts b/src/options/presets/Default.ts index 7881bd14..cdddc624 100644 --- a/src/options/presets/Default.ts +++ b/src/options/presets/Default.ts @@ -11,6 +11,7 @@ import { StringArrayEncoding } from '../../enums/node-transformers/string-array- import { StringArrayWrappersType } from '../../enums/node-transformers/string-array-transformers/StringArrayWrappersType'; export const DEFAULT_PRESET: TInputOptions = Object.freeze({ + advertisement: true, compact: true, config: '', controlFlowFlattening: false, diff --git a/src/options/presets/NoCustomNodes.ts b/src/options/presets/NoCustomNodes.ts index 517f83a3..f05920f2 100644 --- a/src/options/presets/NoCustomNodes.ts +++ b/src/options/presets/NoCustomNodes.ts @@ -10,6 +10,7 @@ import { StringArrayWrappersType } from '../../enums/node-transformers/string-ar import { StringArrayIndexesType } from '../../enums/node-transformers/string-array-transformers/StringArrayIndexesType'; export const NO_ADDITIONAL_NODES_PRESET: TInputOptions = Object.freeze({ + advertisement: true, compact: true, controlFlowFlattening: false, controlFlowFlatteningThreshold: 0, diff --git a/src/utils/AdvertisementUtils.ts b/src/utils/AdvertisementUtils.ts index 992e09a0..98a31df4 100644 --- a/src/utils/AdvertisementUtils.ts +++ b/src/utils/AdvertisementUtils.ts @@ -84,8 +84,14 @@ export class AdvertisementUtils { * Also increments the display count if returning true * * In browser environments, always returns false + * + * @param {boolean} advertisement value of the `advertisement` option */ - public static shouldShowAdvertisement(): boolean { + public static shouldShowAdvertisement(advertisement: boolean): boolean { + if (!advertisement) { + return false; + } + // Don't show in browser - only Node.js CLI if (!this.isNodeEnvironment()) { return false; diff --git a/test/functional-tests/cli/JavaScriptObfuscatorCLI.spec.ts b/test/functional-tests/cli/JavaScriptObfuscatorCLI.spec.ts index 6b6355d8..6f7ae029 100644 --- a/test/functional-tests/cli/JavaScriptObfuscatorCLI.spec.ts +++ b/test/functional-tests/cli/JavaScriptObfuscatorCLI.spec.ts @@ -10,6 +10,7 @@ import { ISourceMap } from '../../../src/interfaces/source-code/ISourceMap'; import { StdoutWriteMock } from '../../mocks/StdoutWriteMock'; +import { AdvertisementUtils } from '../../../src/utils/AdvertisementUtils'; import { JavaScriptObfuscatorCLI } from '../../../src/JavaScriptObfuscatorCLIFacade'; import { ProApiClient } from '../../../src/pro-api/ProApiClient'; import { parseSourceMapFromObfuscatedCode } from '../../helpers/parseSourceMapFromObfuscatedCode'; @@ -1356,6 +1357,97 @@ describe('JavaScriptObfuscatorCLI', function (): void { }); }); + describe('`--advertisement` option', () => { + const advertisementText: string = 'JavaScript Obfuscator Pro'; + + let shouldShowAdvertisementStub: sinon.SinonStub, + consoleLogStub: sinon.SinonStub; + + const isAdvertisementLogged = (): boolean => + consoleLogStub + .getCalls() + .some((call) => + call.args.some((arg) => typeof arg === 'string' && arg.includes(advertisementText)) + ); + + beforeEach(() => { + shouldShowAdvertisementStub = sinon + .stub(AdvertisementUtils, 'shouldShowAdvertisement') + .callsFake((advertisement: boolean): boolean => advertisement); + consoleLogStub = sinon.stub(console, 'log'); + }); + + afterEach(() => { + shouldShowAdvertisementStub.restore(); + consoleLogStub.restore(); + rimraf.sync(outputFilePath); + }); + + describe('Variant #1: `--advertisement` option is not set (enabled by default)', () => { + let isAdvertisementShown: boolean; + + beforeEach(async () => { + await JavaScriptObfuscatorCLI.obfuscate([ + 'node', + 'javascript-obfuscator', + fixtureFilePath, + '--output', + outputFilePath + ]); + + isAdvertisementShown = isAdvertisementLogged(); + }); + + it('should show the advertisement message', () => { + assert.isTrue(isAdvertisementShown); + }); + }); + + describe('Variant #2: `--advertisement` option is set to `true`', () => { + let isAdvertisementShown: boolean; + + beforeEach(async () => { + await JavaScriptObfuscatorCLI.obfuscate([ + 'node', + 'javascript-obfuscator', + fixtureFilePath, + '--output', + outputFilePath, + '--advertisement', + 'true' + ]); + + isAdvertisementShown = isAdvertisementLogged(); + }); + + it('should show the advertisement message', () => { + assert.isTrue(isAdvertisementShown); + }); + }); + + describe('Variant #3: `--advertisement` option is set to `false`', () => { + let isAdvertisementShown: boolean; + + beforeEach(async () => { + await JavaScriptObfuscatorCLI.obfuscate([ + 'node', + 'javascript-obfuscator', + fixtureFilePath, + '--output', + outputFilePath, + '--advertisement', + 'false' + ]); + + isAdvertisementShown = isAdvertisementLogged(); + }); + + it('should not show the advertisement message', () => { + assert.isFalse(isAdvertisementShown); + }); + }); + }); + describe('`--pro-api-token` option', () => { let fetchStub: sinon.SinonStub; let proApiFilePath: string; diff --git a/test/functional-tests/issues/issue1448.spec.ts b/test/functional-tests/issues/issue1448.spec.ts new file mode 100644 index 00000000..42039438 --- /dev/null +++ b/test/functional-tests/issues/issue1448.spec.ts @@ -0,0 +1,123 @@ +import * as sinon from 'sinon'; + +import { assert } from 'chai'; + +import { AdvertisementUtils } from '../../../src/utils/AdvertisementUtils'; + +import { JavaScriptObfuscator } from '../../../src/JavaScriptObfuscatorFacade'; + +// +// https://github.com/javascript-obfuscator/javascript-obfuscator/issues/1448 +// +describe('Issue #1448', () => { + const code: string = 'var foo = 1;'; + + describe('`advertisement` option', () => { + let shouldShowAdvertisementStub: sinon.SinonStub, + consoleLogStub: sinon.SinonStub; + + beforeEach(() => { + shouldShowAdvertisementStub = sinon + .stub(AdvertisementUtils, 'shouldShowAdvertisement') + .callsFake((advertisement: boolean): boolean => advertisement); + consoleLogStub = sinon.stub(console, 'log'); + }); + + afterEach(() => { + shouldShowAdvertisementStub.restore(); + consoleLogStub.restore(); + }); + + describe('Variant #1: `advertisement` option is enabled by default', () => { + let isAdvertisementShown: boolean, loggedMessage: string; + + beforeEach(() => { + JavaScriptObfuscator.obfuscate(code); + + isAdvertisementShown = consoleLogStub.called; + loggedMessage = isAdvertisementShown ? String(consoleLogStub.firstCall.args[0]) : ''; + }); + + it('should show the advertisement message', () => { + assert.isTrue(isAdvertisementShown); + }); + + it('should log the JavaScript Obfuscator Pro advertisement message', () => { + assert.include(loggedMessage, 'JavaScript Obfuscator Pro'); + }); + }); + + describe('Variant #2: `advertisement` option is set to `true`', () => { + let isAdvertisementShown: boolean; + + beforeEach(() => { + JavaScriptObfuscator.obfuscate(code, { advertisement: true }); + + isAdvertisementShown = consoleLogStub.called; + }); + + it('should show the advertisement message', () => { + assert.isTrue(isAdvertisementShown); + }); + }); + + describe('Variant #3: `advertisement` option is set to `false`', () => { + let isAdvertisementShown: boolean; + + beforeEach(() => { + JavaScriptObfuscator.obfuscate(code, { advertisement: false }); + + isAdvertisementShown = consoleLogStub.called; + }); + + it('should not show the advertisement message', () => { + assert.isFalse(isAdvertisementShown); + }); + + it('should pass the disabled `advertisement` flag to the display check', () => { + assert.isTrue(shouldShowAdvertisementStub.calledWith(false)); + }); + }); + + describe('Variant #4: `advertisement` option is enabled but display conditions are not met', () => { + let isAdvertisementShown: boolean; + + beforeEach(() => { + shouldShowAdvertisementStub.returns(false); + + JavaScriptObfuscator.obfuscate(code, { advertisement: true }); + + isAdvertisementShown = consoleLogStub.called; + }); + + it('should not show the advertisement message', () => { + assert.isFalse(isAdvertisementShown); + }); + }); + + describe('Variant #5: obfuscation of code that produces custom code helpers', () => { + const stringHeavyCode: string = 'var foo = \'long string value for the array\'; console.log(foo);'; + + let advertisementDisplayCount: number; + + beforeEach(() => { + JavaScriptObfuscator.obfuscate(stringHeavyCode, { + stringArray: true, + stringArrayThreshold: 1, + stringArrayWrappersCount: 1 + }); + + advertisementDisplayCount = consoleLogStub + .getCalls() + .filter((call) => + typeof call.args[0] === 'string' && + call.args[0].includes('JavaScript Obfuscator Pro is now available') + ).length; + }); + + it('should show the advertisement message exactly once', () => { + assert.strictEqual(advertisementDisplayCount, 1); + }); + }); + }); +}); diff --git a/test/index.spec.ts b/test/index.spec.ts index 8ca8a265..07792b1d 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -90,6 +90,7 @@ import './functional-tests/issues/issue424.spec'; import './functional-tests/issues/issue437.spec'; import './functional-tests/issues/issue1419.spec'; import './functional-tests/issues/issue1437.spec'; +import './functional-tests/issues/issue1448.spec'; import './functional-tests/javascript-obfuscator/JavaScriptObfuscator.spec'; import './functional-tests/node-transformers/control-flow-transformers/block-statement-control-flow-transformer/BlockStatementControlFlowTransformer.spec'; import './functional-tests/node-transformers/control-flow-transformers/control-flow-replacers/binary-expression-control-flow-replacer/BinaryExpressionControlFlowReplacer.spec'; diff --git a/test/unit-tests/utils/AdvertisementUtils.spec.ts b/test/unit-tests/utils/AdvertisementUtils.spec.ts index 079b1b76..68b0bb60 100644 --- a/test/unit-tests/utils/AdvertisementUtils.spec.ts +++ b/test/unit-tests/utils/AdvertisementUtils.spec.ts @@ -121,7 +121,7 @@ describe('AdvertisementUtils', () => { process.stdout.isTTY = false; // Clear CI env vars delete process.env.CI; - assert.isFalse(AdvertisementUtils.shouldShowAdvertisement()); + assert.isFalse(AdvertisementUtils.shouldShowAdvertisement(true)); }); }); @@ -129,7 +129,7 @@ describe('AdvertisementUtils', () => { it('should return false in CI environment', () => { process.stdout.isTTY = true; process.env.CI = 'true'; - assert.isFalse(AdvertisementUtils.shouldShowAdvertisement()); + assert.isFalse(AdvertisementUtils.shouldShowAdvertisement(true)); }); }); @@ -153,34 +153,34 @@ describe('AdvertisementUtils', () => { it('should return true for first 5 calls', () => { for (let i = 0; i < 5; i++) { - assert.isTrue(AdvertisementUtils.shouldShowAdvertisement(), `Call ${i + 1} should return true`); + assert.isTrue(AdvertisementUtils.shouldShowAdvertisement(true), `Call ${i + 1} should return true`); } }); it('should return false after 5 calls', () => { // Exhaust the counter for (let i = 0; i < 5; i++) { - AdvertisementUtils.shouldShowAdvertisement(); + AdvertisementUtils.shouldShowAdvertisement(true); } // 6th call should return false - assert.isFalse(AdvertisementUtils.shouldShowAdvertisement()); + assert.isFalse(AdvertisementUtils.shouldShowAdvertisement(true)); }); it('should increment counter on each call', () => { - AdvertisementUtils.shouldShowAdvertisement(); + AdvertisementUtils.shouldShowAdvertisement(true); assert.strictEqual(readConfig().adDisplayCount, 1); - AdvertisementUtils.shouldShowAdvertisement(); + AdvertisementUtils.shouldShowAdvertisement(true); assert.strictEqual(readConfig().adDisplayCount, 2); - AdvertisementUtils.shouldShowAdvertisement(); + AdvertisementUtils.shouldShowAdvertisement(true); assert.strictEqual(readConfig().adDisplayCount, 3); }); it('should set first display timestamp on first call', () => { const beforeTime = Date.now(); - AdvertisementUtils.shouldShowAdvertisement(); + AdvertisementUtils.shouldShowAdvertisement(true); const afterTime = Date.now(); const timestamp = readConfig().adFirstDisplayTime; @@ -192,9 +192,9 @@ describe('AdvertisementUtils', () => { it('should reset counter after 3 days', () => { // Exhaust counter for (let i = 0; i < 5; i++) { - AdvertisementUtils.shouldShowAdvertisement(); + AdvertisementUtils.shouldShowAdvertisement(true); } - assert.isFalse(AdvertisementUtils.shouldShowAdvertisement()); + assert.isFalse(AdvertisementUtils.shouldShowAdvertisement(true)); // Simulate 3 days passing by setting old timestamp const threeDaysAgo = Date.now() - 3 * 24 * 60 * 60 * 1000 - 1000; @@ -203,7 +203,7 @@ describe('AdvertisementUtils', () => { writeConfig(data); // Should return true again after reset - assert.isTrue(AdvertisementUtils.shouldShowAdvertisement()); + assert.isTrue(AdvertisementUtils.shouldShowAdvertisement(true)); // Counter should be reset to 1 assert.strictEqual(readConfig().adDisplayCount, 1); }); @@ -211,7 +211,7 @@ describe('AdvertisementUtils', () => { it('should not reset counter before 3 days', () => { // Exhaust counter for (let i = 0; i < 5; i++) { - AdvertisementUtils.shouldShowAdvertisement(); + AdvertisementUtils.shouldShowAdvertisement(true); } // Simulate 2 days passing (less than 3 days) @@ -221,7 +221,34 @@ describe('AdvertisementUtils', () => { writeConfig(data); // Should still return false - assert.isFalse(AdvertisementUtils.shouldShowAdvertisement()); + assert.isFalse(AdvertisementUtils.shouldShowAdvertisement(true)); + }); + }); + + describe('Variant #4: `advertisement` option is disabled', () => { + beforeEach(() => { + deleteConfig(); + (AdvertisementUtils as any).configPath = null; + // Ensure conditions that would otherwise show the advertisement + process.stdout.isTTY = true; + delete process.env.CI; + delete process.env.GITHUB_ACTIONS; + delete process.env.TRAVIS; + delete process.env.GITLAB_CI; + }); + + afterEach(() => { + deleteConfig(); + (AdvertisementUtils as any).configPath = null; + }); + + it('should return false when `advertisement` is `false`, even if all other conditions are met', () => { + assert.isFalse(AdvertisementUtils.shouldShowAdvertisement(false)); + }); + + it('should not touch the display counter when `advertisement` is `false`', () => { + AdvertisementUtils.shouldShowAdvertisement(false); + assert.isUndefined(readConfig().adDisplayCount); }); }); });