2929 * WordPress shines bright
3030 *
3131 * # Generate an image from a prompt
32- * $ wp ai generate image "A futuristic WordPress logo" --output =logo.png
32+ * $ wp ai generate image "A futuristic WordPress logo" --destination-file =logo.png
3333 * Success: Image saved to logo.png
3434 *
3535 * # Check if a prompt is supported
4040 */
4141class AI_Command extends WP_CLI_Command {
4242
43- /**
44- * Maximum binary image size in bytes (50MB).
45- */
46- const MAX_IMAGE_SIZE_BYTES = 52428800 ; // 50 * 1024 * 1024
47-
48- /**
49- * Dummy prompt used for capability checking.
50- *
51- * This constant provides a consistent prompt value when checking AI capabilities.
52- * The specific content doesn't affect capability detection, which is based on
53- * configured providers and their available features.
54- */
55- const CAPABILITY_CHECK_PROMPT = 'capability-check ' ;
56-
57- /**
58- * System directories that should be protected from file writes.
59- */
60- const FORBIDDEN_PATHS = array (
61- // Unix/Linux system directories
62- '/etc ' ,
63- '/bin ' ,
64- '/usr/bin ' ,
65- '/sbin ' ,
66- '/usr/sbin ' ,
67- '/boot ' ,
68- '/sys ' ,
69- '/proc ' ,
70- // Windows system directories (case-insensitive)
71- 'C: \\Windows ' ,
72- 'C: \\Program Files ' ,
73- 'C: \\Program Files (x86) ' ,
74- );
75-
76- /**
77- * Maximum size for base64-encoded image data.
78- * Base64 encoding increases size by ~33%, so 50MB binary = ~67MB base64.
79- * Using 70MB as safe upper bound.
80- */
81- const MAX_IMAGE_SIZE_BASE64 = 70000000 ;
82-
8343 /**
8444 * Generates AI content.
8545 *
@@ -117,9 +77,12 @@ class AI_Command extends WP_CLI_Command {
11777 * [--system-instruction=<instruction>]
11878 * : System instruction to guide the AI's behavior.
11979 *
120- * [--output =<file>]
80+ * [--destination-file =<file>]
12181 * : For image generation, path to save the generated image.
12282 *
83+ * [--stdout]
84+ * Output the whole image using standard output (incompatible with --destination-file=)
85+ *
12386 * [--format=<format>]
12487 * : Output format for text.
12588 * ---
@@ -150,7 +113,7 @@ class AI_Command extends WP_CLI_Command {
150113 * $ wp ai generate image "A minimalist WordPress logo" --output=wp-logo.png
151114 *
152115 * @param array{0: string, 1: string} $args Positional arguments.
153- * @param array{model: string, provider: string, temperature: float, 'top-p': float, 'top-k': int, 'max-tokens': int, 'system-instruction': string, output : string, format: string} $assoc_args Associative arguments.
116+ * @param array{model: string, provider: string, temperature: float, 'top-p': float, 'top-k': int, 'max-tokens': int, 'system-instruction': string, 'destination-file' : string, stdout: bool , format: string} $assoc_args Associative arguments.
154117 * @return void
155118 */
156119 public function generate ( $ args , $ assoc_args ) {
@@ -322,7 +285,7 @@ public function status( $args, $assoc_args ) {
322285
323286 try {
324287 // Create a builder to check capabilities (using constant for consistency)
325- $ builder = AI_Client::prompt ( self :: CAPABILITY_CHECK_PROMPT );
288+ $ builder = AI_Client::prompt ();
326289
327290 // Check each capability
328291 $ capabilities = array (
@@ -388,8 +351,8 @@ private function generate_text( $builder, $assoc_args ) {
388351 /**
389352 * Generates an image from the prompt builder.
390353 *
391- * @param \WordPress\AI_Client\Builders\Prompt_Builder $builder The prompt builder.
392- * @param array{output : string} $assoc_args Associative arguments.
354+ * @param \WordPress\AI_Client\Builders\Prompt_Builder $builder The prompt builder.
355+ * @param array{'destination-file' : string, stdout: bool} $assoc_args Associative arguments.
393356 * @return void
394357 */
395358 private function generate_image ( $ builder , $ assoc_args ) {
@@ -398,75 +361,66 @@ private function generate_image( $builder, $assoc_args ) {
398361 WP_CLI ::error ( 'Image generation is not supported. Make sure AI provider credentials are configured. ' );
399362 }
400363
401- $ image_file = $ builder ->generate_image ();
402-
403- if ( isset ( $ assoc_args ['output ' ] ) ) {
404- $ output_path = $ assoc_args ['output ' ];
364+ if ( ! empty ( $ assoc_args ['stdout ' ] ) && ! empty ( $ assoc_args ['dir ' ] ) ) {
365+ WP_CLI ::error ( '--stdout and --destination-file cannot be used together. ' );
366+ }
405367
406- // Resolve the full real path
407- $ parent_dir = dirname ( $ output_path );
368+ if ( isset ( $ assoc_args ['destination-file ' ] ) ) {
369+ $ output_path = $ assoc_args ['destination-file ' ];
370+ $ parent_dir = dirname ( $ output_path );
408371
409- // Check if parent directory exists
410- if ( ! file_exists ( $ parent_dir ) || ! is_dir ( $ parent_dir ) ) {
372+ if ( ! is_dir ( $ parent_dir ) ) {
411373 WP_CLI ::error ( 'Invalid output directory. Directory does not exist: ' . $ parent_dir );
412374 }
375+ }
413376
414- // Resolve the real path to prevent traversal attacks
415- $ real_parent_dir = realpath ( $ parent_dir );
416- if ( false === $ real_parent_dir ) {
417- WP_CLI ::error ( 'Cannot resolve output directory path. ' );
418- }
419-
420- // Reconstruct the output path with the resolved parent directory
421- $ safe_output_path = $ real_parent_dir . DIRECTORY_SEPARATOR . basename ( $ output_path );
422-
423- // Prevent writing to sensitive system directories
424- foreach ( self ::FORBIDDEN_PATHS as $ forbidden ) {
425- // Use case-sensitive check for Unix paths (start with /), case-insensitive for Windows (contain :\)
426- $ is_windows_path = ( false !== strpos ( $ forbidden , ': \\' ) );
427- $ matches = $ is_windows_path
428- ? ( 0 === stripos ( $ real_parent_dir , $ forbidden ) )
429- : ( 0 === strpos ( $ real_parent_dir , $ forbidden ) );
377+ $ image_file = $ builder ->generate_image ();
430378
431- if ( $ matches ) {
432- WP_CLI ::error ( 'Cannot write to system directory: ' . $ safe_output_path );
433- }
434- }
379+ if ( isset ( $ assoc_args ['destination-file ' ] ) ) {
380+ $ output_path = $ assoc_args ['destination-file ' ];
381+ $ output_path = realpath ( dirname ( $ output_path ) ) . DIRECTORY_SEPARATOR . basename ( $ output_path );
435382
436- // Get the image content from data URI
437383 $ data_uri = $ image_file ->getDataUri ();
438384
439- // Extract base64 data from data URI
440385 $ data_parts = $ data_uri ? explode ( ', ' , $ data_uri , 2 ) : [];
441386 if ( count ( $ data_parts ) !== 2 ) {
442387 WP_CLI ::error ( 'Invalid image data received. ' );
443388 }
444389
445- // Validate and decode base64 data
446390 $ base64_data = $ data_parts [1 ];
447391
448- // Check reasonable size limit
449- if ( strlen ( $ base64_data ) > self ::MAX_IMAGE_SIZE_BASE64 ) {
450- WP_CLI ::error ( 'Image data exceeds maximum size limit (50MB). ' );
451- }
452-
453- // Try strict base64 decode - this validates format
454392 // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
455393 $ image_data = base64_decode ( $ base64_data , true );
456394 if ( false === $ image_data ) {
457395 WP_CLI ::error ( 'Invalid base64 image data format. ' );
458396 }
459397
460398 // Save to file
461- $ result = file_put_contents ( $ safe_output_path , $ image_data );
399+ $ result = file_put_contents ( $ output_path , $ image_data );
462400 if ( false === $ result ) {
463- WP_CLI ::error ( 'Failed to save image to ' . $ safe_output_path );
401+ WP_CLI ::error ( 'Failed to save image to ' . $ output_path );
402+ }
403+
404+ WP_CLI ::success ( 'Image saved to ' . $ output_path );
405+ } elseif ( $ assoc_args ['stdout ' ] ) {
406+ $ data_uri = $ image_file ->getDataUri ();
407+
408+ $ data_parts = $ data_uri ? explode ( ', ' , $ data_uri , 2 ) : [];
409+ if ( count ( $ data_parts ) !== 2 ) {
410+ WP_CLI ::error ( 'Invalid image data received. ' );
411+ }
412+
413+ $ base64_data = $ data_parts [1 ];
414+
415+ // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
416+ $ image_data = base64_decode ( $ base64_data , true );
417+ if ( false === $ image_data ) {
418+ WP_CLI ::error ( 'Invalid base64 image data format. ' );
464419 }
465420
466- WP_CLI ::success ( ' Image saved to ' . $ safe_output_path );
421+ WP_CLI ::log ( $ image_data );
467422 } else {
468- // Output data URI
469- WP_CLI ::success ( 'Image generated (data URI): ' );
423+ WP_CLI ::success ( 'Image generated: ' );
470424 WP_CLI ::line ( (string ) $ image_file ->getDataUri () );
471425 }
472426 }
0 commit comments