Skip to content
Merged
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 @@ -4,7 +4,9 @@

import { jest } from '@jest/globals';
import { readFile } from 'node:fs/promises';
import path from 'node:path';

import * as exit from '../../../src/lib/cli/exit';
import { getConfigurationFileOptions } from '../../../src/lib/dev-environment/dev-environment-configuration-file';

// Mock fs operations
Expand All @@ -18,9 +20,12 @@ jest.mock( '../../../src/lib/cli/exit', () => ( {
} ) );

const mockedReadFile = readFile;
const mockedExitWithError = exit.withError;

describe( 'dev-environment-configuration-file', () => {
describe( 'multisite configuration sanitization', () => {
const templateConfigDir = path.join( '/test/dir', '.wpvip' );
const templateConfigPath = path.join( templateConfigDir, 'vip-dev-env.yml.ejs' );
const createConfigContent = multisiteValue => `
configuration-version: 1
slug: test-site
Expand All @@ -32,6 +37,9 @@ app-code: demo
beforeEach( () => {
jest.clearAllMocks();
jest.resetAllMocks();
mockedExitWithError.mockImplementation( message => {
throw new Error( message );
} );
// Mock process.cwd() to return a known directory
jest.spyOn( process, 'cwd' ).mockReturnValue( '/test/dir' );
// Default mock to reject all file reads
Expand Down Expand Up @@ -126,5 +134,38 @@ app-code: demo

expect( result ).toEqual( {} );
} );

it( 'should replace supported configDir placeholders in template configuration files', async () => {
const configContent = `
configuration-version: 1
slug: test-site
overrides: |
services:
php:
volumes:
- <%= configDir %>/..:/wp/wp-content/plugins/my-integration
`;

mockedReadFile.mockResolvedValueOnce( configContent );

const result = await getConfigurationFileOptions();

expect( result.overrides ).toContain(
`${ templateConfigDir }/..:/wp/wp-content/plugins/my-integration`
);
} );

it( 'should reject arbitrary EJS syntax in template configuration files', async () => {
const configContent = `
configuration-version: 1
slug: <%= process.env.SLUG %>
`;

mockedReadFile.mockResolvedValueOnce( configContent );

await expect( getConfigurationFileOptions() ).rejects.toThrow(
`EJS JavaScript is not supported in dev-env configuration file ${ templateConfigPath }`
);
} );
} );
} );
30 changes: 21 additions & 9 deletions src/lib/dev-environment/dev-environment-configuration-file.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import chalk from 'chalk';
import debugLib from 'debug';
import ejs from 'ejs';
import yaml, { FAILSAFE_SCHEMA } from 'js-yaml';
import { readFile } from 'node:fs/promises';
import path from 'node:path';
Expand All @@ -23,6 +22,8 @@ const debug = debugLib( '@automattic/vip:bin:dev-environment' );

export const CONFIGURATION_FILE_NAME = 'vip-dev-env.yml';
export const CONFIGURATION_TEMPLATE_FILE_NAME = 'vip-dev-env.yml.ejs';
const ALLOWED_CONFIG_DIR_TEMPLATE = /<%=\s*configDir\s*%>/g;
const EJS_TEMPLATE_TAG = /<%[\s\S]*?%>/;
Comment on lines +25 to +26

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

n/a because it won't execute


export async function getConfigurationFileOptions(): Promise< ConfigurationFileOptions > {
const configurationFile = await findConfigurationFile();
Expand Down Expand Up @@ -249,19 +250,30 @@ async function findConfigurationFile(): Promise<
}

for ( const { dir, file, template } of locations ) {
let contents: string;
try {
// eslint-disable-next-line no-await-in-loop
const contents = await readFile( file, 'utf8' );
if ( template ) {
const rendered = ejs.render( contents, { configDir: dir } );
return { configurationPath: file, configurationContents: rendered };
}

return { configurationPath: file, configurationContents: contents };
contents = await readFile( file, 'utf8' );
} catch ( error ) {
const err = error instanceof Error ? error : new Error( 'Unknown error' );
debug( `Error reading or rendering file ${ file }: ${ err.message }` );
debug( `Error reading file ${ file }: ${ err.message }` );
continue;
}

if ( template ) {
const stripped = contents.replace( ALLOWED_CONFIG_DIR_TEMPLATE, '' );
if ( EJS_TEMPLATE_TAG.test( stripped ) ) {
exit.withError(
`EJS JavaScript is not supported in dev-env configuration file ${ file }. ` +
'Use plain YAML, or only the supported <%= configDir %> placeholder.'
);
}

const rendered = contents.replace( ALLOWED_CONFIG_DIR_TEMPLATE, () => dir );
return { configurationPath: file, configurationContents: rendered };
}
Comment thread
Copilot marked this conversation as resolved.

return { configurationPath: file, configurationContents: contents };
}

return false;
Expand Down