From df634c08359fdc320b8caeee84d1a74964ffb92b Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Fri, 23 Oct 2026 11:08:00 +0100 Subject: [PATCH] Studio Code: Refuse a file path given as wp_cli --post_content instead of rewriting the command Co-Authored-By: Claude Opus 5.5 --- apps/cli/ai/tools/wp-cli.ts | 13 +--- .../cli/lib/tests/wp-cli-post-content.test.ts | 75 +++++-------------- apps/cli/lib/wp-cli-post-content.ts | 54 ++----------- 3 files changed, 28 insertions(+), 114 deletions(-) diff --git a/apps/cli/ai/tools/wp-cli.ts b/apps/cli/ai/tools/wp-cli.ts index 80e8e10e12..121ba1bbea 100644 --- a/apps/cli/ai/tools/wp-cli.ts +++ b/apps/cli/ai/tools/wp-cli.ts @@ -1,10 +1,7 @@ import { Type } from 'typebox'; import { connectToDaemon, disconnectFromDaemon } from 'cli/lib/daemon-client'; import { runWpCliCommand } from 'cli/lib/run-wp-cli-command'; -import { - getUnsupportedWpCliPostContentMessage, - routePostContentFile, -} from 'cli/lib/wp-cli-post-content'; +import { getUnsupportedWpCliPostContentMessage } from 'cli/lib/wp-cli-post-content'; import { defineTool, REMOTE_OVERWRITE } from './define-tool'; import { resolveSite } from './utils'; @@ -167,9 +164,7 @@ export const runWpCliTool = defineTool( try { await connectToDaemon(); - const splitArgs = splitCommandArgs( args.command ); - const routed = routePostContentFile( splitArgs, site.path ); - const wpCliArgs = routed?.args ?? splitArgs; + const wpCliArgs = splitCommandArgs( args.command ); const unsupportedOptionMessage = getUnsupportedWpCliOptionMessage( wpCliArgs ); if ( unsupportedOptionMessage ) { throw new Error( unsupportedOptionMessage ); @@ -184,9 +179,7 @@ export const runWpCliTool = defineTool( const stdout = await command.response.stdoutText; const stderr = await command.response.stderrText; - let output = routed - ? `Read the post content from ${ routed.file }, the file \`--post_content\` named.\n` - : ''; + let output = ''; if ( stdout ) { output += stdout; } diff --git a/apps/cli/lib/tests/wp-cli-post-content.test.ts b/apps/cli/lib/tests/wp-cli-post-content.test.ts index 747adce0c1..f6fa5f5e5d 100644 --- a/apps/cli/lib/tests/wp-cli-post-content.test.ts +++ b/apps/cli/lib/tests/wp-cli-post-content.test.ts @@ -1,10 +1,4 @@ -import { mkdirSync, mkdtempSync, writeFileSync } from 'fs'; -import os from 'os'; -import path from 'path'; -import { - getUnsupportedWpCliPostContentMessage, - routePostContentFile, -} from 'cli/lib/wp-cli-post-content'; +import { getUnsupportedWpCliPostContentMessage } from 'cli/lib/wp-cli-post-content'; describe( 'getUnsupportedWpCliPostContentMessage', () => { it( 'ignores commands other than post create and update', () => { @@ -65,62 +59,31 @@ describe( 'getUnsupportedWpCliPostContentMessage', () => { ] ) ).toContain( 'does not run in a shell' ); } ); -} ); - -describe( 'routePostContentFile', () => { - const sitePath = mkdtempSync( path.join( os.tmpdir(), 'post-content-path-' ) ); - mkdirSync( path.join( sitePath, 'tmp' ) ); - writeFileSync( path.join( sitePath, 'tmp', 'header.html' ), '' ); - - it( 'passes an absolute file path as the file WP-CLI reads content from', () => { - const file = path.join( sitePath, 'tmp', 'header.html' ); - expect( - routePostContentFile( [ 'post', 'update', '5', `--post_content=${ file }` ], sitePath ) - ).toEqual( { args: [ 'post', 'update', '5', file ], file } ); - } ); - it( 'keeps the flags around a relative path', () => { - expect( - routePostContentFile( - [ 'post', 'create', '--post_type=page', '--post_content=tmp/header.html --porcelain' ], - sitePath - ) - ).toEqual( { - args: [ 'post', 'create', '--post_type=page', '--porcelain', 'tmp/header.html' ], - file: 'tmp/header.html', - } ); - } ); - - it( 'leaves content that only looks like a path, and other commands, alone', () => { + it( 'refuses a file path, which WP-CLI would save as the content', () => { for ( const content of [ - 'tmp/missing.html', - 'tmp', - 'See tmp/header.html', - 'tmp/header.html and more', - '

tmp/header.html

', + '/Users/me/Studio/site/tmp/header.html', + 'tmp/header.html --porcelain', + 'tmp/styles.json', ] ) { expect( - routePostContentFile( [ 'post', 'create', `--post_content=${ content }` ], sitePath ) - ).toBeUndefined(); + getUnsupportedWpCliPostContentMessage( [ + 'post', + 'update', + '5', + `--post_content=${ content }`, + ] ) + ).toContain( 'not a file path' ); } - expect( - routePostContentFile( - [ 'option', 'update', 'x', '--post_content=tmp/header.html' ], - sitePath - ) - ).toBeUndefined(); - } ); - - it( 'leaves a file outside the site folder alone', () => { - const outside = mkdtempSync( path.join( os.tmpdir(), 'post-content-outside-' ) ); - writeFileSync( path.join( outside, 'notes.html' ), 'x' ); - for ( const file of [ - path.join( outside, 'notes.html' ), - `../${ path.basename( outside ) }/notes.html`, + for ( const content of [ + 'tmp', + 'See tmp/header.html', + '

tmp/header.html

', + '

Hi

', ] ) { expect( - routePostContentFile( [ 'post', 'create', `--post_content=${ file }` ], sitePath ) - ).toBeUndefined(); + getUnsupportedWpCliPostContentMessage( [ 'post', 'create', `--post_content=${ content }` ] ) + ).toBeNull(); } } ); } ); diff --git a/apps/cli/lib/wp-cli-post-content.ts b/apps/cli/lib/wp-cli-post-content.ts index 592afba3fa..c84d7c780a 100644 --- a/apps/cli/lib/wp-cli-post-content.ts +++ b/apps/cli/lib/wp-cli-post-content.ts @@ -1,6 +1,3 @@ -import { existsSync, statSync } from 'fs'; -import path from 'path'; - const UNSUPPORTED_WP_CLI_POST_CONTENT_MESSAGE = 'Unsupported `--post_content` value: `wp_cli` does not run in a shell. Do not use `$(cat file)` or backticks. Pass the literal post content directly, or write large content to a scratch file inside the site directory and pass its path as a positional argument (`wp post create `).'; @@ -34,6 +31,12 @@ export function getUnsupportedWpCliPostContentMessage( args: string[] ): string return UNSUPPORTED_WP_CLI_POST_CONTENT_MESSAGE; } + // WP-CLI saves a path given as `--post_content` as the content itself and + // reports success, which wipes the post. + if ( /^[^\s<]+\.(html?|txt|md|json)(\s+--\S+)*$/i.test( trimmedContent ) ) { + return 'Nothing was saved: `--post_content` takes the content itself, not a file path. To save the content of a file, pass its path as a positional argument: `post create ` or `post update `.'; + } + // Block markup and HTML never start with `{`, so content that does is JSON, // such as a Global Styles post. WordPress ignores JSON post content that // does not parse, while WP-CLI still saves it and reports success. @@ -48,48 +51,3 @@ export function getUnsupportedWpCliPostContentMessage( args: string[] ): string return null; } - -/** - * `post create` / `post update` with `--post_content=`, where the value - * is the path of a file in the site folder (absolute or relative to it): the same command - * with the file as the positional argument WP-CLI reads content from, so the - * post gets the file's content rather than its path. Unquoted content runs to - * the end of the command, so flags after the path (`--porcelain`) are split - * back out. Undefined for any other command. - */ -export function routePostContentFile( - args: string[], - sitePath?: string -): { args: string[]; file: string } | undefined { - const isPostCommand = - args[ 0 ] === 'post' && ( args[ 1 ] === 'create' || args[ 1 ] === 'update' ); - const contentIndex = args.findIndex( ( arg ) => arg.startsWith( '--post_content=' ) ); - if ( ! isPostCommand || contentIndex === -1 ) { - return undefined; - } - const [ file, ...flags ] = args[ contentIndex ] - .slice( '--post_content='.length ) - .trim() - .split( /\s+/ ); - if ( ! file || file.includes( '<' ) || flags.some( ( flag ) => ! flag.startsWith( '--' ) ) ) { - return undefined; - } - if ( ! sitePath ) { - return undefined; - } - // Only files in the site folder, which WP-CLI reads from inside the site. - const filePath = path.resolve( sitePath, file ); - const relative = path.relative( sitePath, filePath ); - if ( - relative.startsWith( '..' ) || - path.isAbsolute( relative ) || - ! existsSync( filePath ) || - ! statSync( filePath ).isFile() - ) { - return undefined; - } - return { - args: [ ...args.slice( 0, contentIndex ), ...args.slice( contentIndex + 1 ), ...flags, file ], - file, - }; -}