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
13 changes: 3 additions & 10 deletions apps/cli/ai/tools/wp-cli.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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 );
Expand All @@ -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;
}
Expand Down
75 changes: 19 additions & 56 deletions apps/cli/lib/tests/wp-cli-post-content.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -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' ), '<!-- wp:paragraph -->' );

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',
'<p>tmp/header.html</p>',
'/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',
'<p>tmp/header.html</p>',
'<!-- wp:paragraph --><p>Hi</p><!-- /wp:paragraph -->',
] ) {
expect(
routePostContentFile( [ 'post', 'create', `--post_content=${ file }` ], sitePath )
).toBeUndefined();
getUnsupportedWpCliPostContentMessage( [ 'post', 'create', `--post_content=${ content }` ] )
).toBeNull();
}
} );
} );
54 changes: 6 additions & 48 deletions apps/cli/lib/wp-cli-post-content.ts
Original file line number Diff line number Diff line change
@@ -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 <file>`).';

Expand Down Expand Up @@ -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 <file>` or `post update <id> <file>`.';
}

// 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.
Expand All @@ -48,48 +51,3 @@ export function getUnsupportedWpCliPostContentMessage( args: string[] ): string

return null;
}

/**
* `post create` / `post update` with `--post_content=<file>`, 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,
};
}
Loading