diff --git a/packages/create-docusaurus/package.json b/packages/create-docusaurus/package.json index 46ae2253e1b4..f4ca79dcd026 100755 --- a/packages/create-docusaurus/package.json +++ b/packages/create-docusaurus/package.json @@ -23,6 +23,7 @@ "license": "MIT", "dependencies": { "@docusaurus/logger": "2.0.0-beta.20", + "@docusaurus/utils": "2.0.0-beta.20", "commander": "^5.1.0", "fs-extra": "^10.1.0", "lodash": "^4.17.21", diff --git a/packages/create-docusaurus/src/index.ts b/packages/create-docusaurus/src/index.ts index 27d53f50fd59..b996c3e90b6b 100755 --- a/packages/create-docusaurus/src/index.ts +++ b/packages/create-docusaurus/src/index.ts @@ -13,6 +13,7 @@ import logger from '@docusaurus/logger'; import shell from 'shelljs'; import prompts, {type Choice} from 'prompts'; import supportsColor from 'supports-color'; +import {escapeShellArg} from '@docusaurus/utils'; type CLIOptions = { packageManager?: PackageManager; @@ -463,9 +464,11 @@ export default async function init( logger.info('Creating new Docusaurus project...'); if (source.type === 'git') { - logger.info`Cloning Git template url=${source.url}...`; - const command = await getGitCommand(source.strategy); - if (shell.exec(`${command} ${source.url} ${dest}`).code !== 0) { + const gitCommand = await getGitCommand(source.strategy); + const gitCloneCommand = `${gitCommand} ${escapeShellArg( + source.url, + )} ${escapeShellArg(dest)}`; + if (shell.exec(gitCloneCommand).code !== 0) { logger.error`Cloning Git template failed!`; process.exit(1); } diff --git a/packages/docusaurus-utils/src/__tests__/shellUtils.test.ts b/packages/docusaurus-utils/src/__tests__/shellUtils.test.ts new file mode 100644 index 000000000000..adc64d944350 --- /dev/null +++ b/packages/docusaurus-utils/src/__tests__/shellUtils.test.ts @@ -0,0 +1,19 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import {escapeShellArg} from '../shellUtils'; + +describe('shellUtils', () => { + it('escapeShellArg', () => { + expect(escapeShellArg('hello')).toBe("'hello'"); + expect(escapeShellArg('*')).toBe("'*'"); + expect(escapeShellArg('hello world')).toBe("'hello world'"); + expect(escapeShellArg("'hello'")).toBe("\\''hello'\\'"); + expect(escapeShellArg('$(pwd)')).toBe("'$(pwd)'"); + expect(escapeShellArg('hello$(pwd)')).toBe("'hello$(pwd)'"); + }); +}); diff --git a/packages/docusaurus-utils/src/gitUtils.ts b/packages/docusaurus-utils/src/gitUtils.ts index 4d7ffde633ee..00e15658c9a4 100644 --- a/packages/docusaurus-utils/src/gitUtils.ts +++ b/packages/docusaurus-utils/src/gitUtils.ts @@ -97,26 +97,19 @@ export function getFileCommitDate( ); } - let formatArg = '--format=%ct'; - if (includeAuthor) { - formatArg += ',%an'; - } - - let extraArgs = '--max-count=1'; - if (age === 'oldest') { - // --follow is necessary to follow file renames - // --diff-filter=A ensures we only get the commit which (A)dded the file - extraArgs += ' --follow --diff-filter=A'; - } + const args = [ + `--format=%ct${includeAuthor ? ',%an' : ''}`, + '--max-count=1', + age === 'oldest' ? '--follow --diff-filter=A' : undefined, + ] + .filter(Boolean) + .join(' '); - const result = shell.exec( - `git log ${extraArgs} ${formatArg} -- "${path.basename(file)}"`, - { - // Setting cwd is important, see: https://github.com/facebook/docusaurus/pull/5048 - cwd: path.dirname(file), - silent: true, - }, - ); + const result = shell.exec(`git log ${args} -- "${path.basename(file)}"`, { + // Setting cwd is important, see: https://github.com/facebook/docusaurus/pull/5048 + cwd: path.dirname(file), + silent: true, + }); if (result.code !== 0) { throw new Error( `Failed to retrieve the git history for file "${file}" with exit code ${result.code}: ${result.stderr}`, diff --git a/packages/docusaurus-utils/src/index.ts b/packages/docusaurus-utils/src/index.ts index fb2c66435ea2..520f5f73aab2 100644 --- a/packages/docusaurus-utils/src/index.ts +++ b/packages/docusaurus-utils/src/index.ts @@ -96,6 +96,7 @@ export { createAbsoluteFilePathMatcher, } from './globUtils'; export {getFileLoaderUtils} from './webpackUtils'; +export {escapeShellArg} from './shellUtils'; export { getDataFilePath, getDataFileData, diff --git a/packages/docusaurus-utils/src/shellUtils.ts b/packages/docusaurus-utils/src/shellUtils.ts new file mode 100644 index 000000000000..25536a3c1fed --- /dev/null +++ b/packages/docusaurus-utils/src/shellUtils.ts @@ -0,0 +1,18 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +// TODO move from shelljs to execa later? +// Execa is well maintained and widely used +// Even shelljs recommends execa for security / escaping: +// https://github.com/shelljs/shelljs/wiki/Security-guidelines + +// Inspired by https://github.com/xxorax/node-shell-escape/blob/master/shell-escape.js +export function escapeShellArg(s: string): string { + let res = `'${s.replace(/'/g, "'\\''")}'`; + res = res.replace(/^(?:'')+/g, '').replace(/\\'''/g, "\\'"); + return res; +} diff --git a/project-words.txt b/project-words.txt index 2e029e7f7c3f..9ca37195ee29 100644 --- a/project-words.txt +++ b/project-words.txt @@ -87,6 +87,8 @@ esbuild eslintcache estree evaluable +execa +Execa externalwaiting failfast fbid