@@ -245,7 +245,7 @@ private function createExecutionPlan( array $blueprint ): array {
245245 if ( ! empty ( $ blueprint ['themes ' ] ) && is_array ( $ blueprint ['themes ' ] ) ) {
246246 foreach ( $ blueprint ['themes ' ] as $ themeRef ) {
247247 if ( is_string ( $ themeRef ) ) {
248- $ plan [] = [
248+ $ step = [
249249 'name ' => 'installTheme ' ,
250250 'args ' => [
251251 'source ' => $ themeRef ,
@@ -255,7 +255,7 @@ private function createExecutionPlan( array $blueprint ): array {
255255 ];
256256 } elseif ( is_array ( $ themeRef ) && isset ( $ themeRef ['source ' ] ) && is_string ( $ themeRef ['source ' ] ) ) {
257257 // Pass through the raw definition for extensibility.
258- $ plan [] = [
258+ $ step = [
259259 'name ' => 'installTheme ' ,
260260 'args ' => [
261261 'source ' => $ themeRef ['source ' ],
@@ -267,14 +267,20 @@ private function createExecutionPlan( array $blueprint ): array {
267267 } else {
268268 throw new InvalidArgumentException ( 'Invalid theme reference format in "themes" array. ' );
269269 }
270+
271+ $ plan [] = $ step ;
272+ $ error = $ this ->validateDataSource ( $ step ['args ' ]['source ' ], 'wp-content/themes/* ' );
273+ if ( $ error ) {
274+ $ errors ['themes ' ] = [$ error ];
275+ }
270276 }
271277 }
272278
273279 // 5. activeTheme (install and activate)
274280 if ( isset ( $ blueprint ['activeTheme ' ] ) ) {
275281 $ themeRef = $ blueprint ['activeTheme ' ];
276282 if ( is_string ( $ themeRef ) ) {
277- $ plan [] = [
283+ $ step = [
278284 'name ' => 'installTheme ' ,
279285 'args ' => [
280286 'source ' => $ themeRef ,
@@ -283,9 +289,9 @@ private function createExecutionPlan( array $blueprint ): array {
283289 ]
284290 ];
285291 } elseif ( is_array ( $ themeRef ) && isset ( $ themeRef ['source ' ] ) && is_string ( $ themeRef ['source ' ] ) ) {
286- $ plan [] = [
292+ $ step = [
287293 'name ' => 'installTheme ' ,
288- 'args ' => [
294+ 'args ' => [
289295 'source ' => $ themeRef ['source ' ],
290296 'active ' => true ,
291297 'importStarterContent ' => $ themeRef ['importStarterContent ' ] ?? false ,
@@ -295,6 +301,12 @@ private function createExecutionPlan( array $blueprint ): array {
295301 } else {
296302 throw new InvalidArgumentException ( 'Invalid theme reference format for "activeTheme". ' );
297303 }
304+
305+ $ plan [] = $ step ;
306+ $ error = $ this ->validateDataSource ( $ step ['args ' ]['source ' ], 'wp-content/themes/* ' );
307+ if ( $ error ) {
308+ $ errors ['themes ' ] = [$ error ];
309+ }
298310 }
299311
300312 // 6. plugins
@@ -309,6 +321,11 @@ private function createExecutionPlan( array $blueprint ): array {
309321 'name ' => 'installPlugin ' ,
310322 'args ' => $ pluginDef
311323 ];
324+
325+ $ error = $ this ->validateDataSource ( $ pluginDef ['source ' ], 'wp-content/plugins/* ' );
326+ if ( $ error ) {
327+ $ errors ['plugins ' ] = [$ error ];
328+ }
312329 }
313330 }
314331
@@ -323,6 +340,14 @@ private function createExecutionPlan( array $blueprint ): array {
323340 'name ' => 'importMedia ' ,
324341 'args ' => [ 'media ' => $ blueprint ['media ' ] ]
325342 ];
343+
344+ foreach ( $ blueprint ['media ' ] as $ mediaDef ) {
345+ $ source = is_array ( $ mediaDef ) ? $ mediaDef ['source ' ] : $ mediaDef ;
346+ $ error = $ this ->validateDataSource ( $ source , 'wp-content/uploads/* ' );
347+ if ( $ error ) {
348+ $ errors ['media ' ] = [$ error ];
349+ }
350+ }
326351 }
327352
328353 // 9. siteLanguage
@@ -364,6 +389,31 @@ private function createExecutionPlan( array $blueprint ): array {
364389 'name ' => 'importContent ' ,
365390 'args ' => [ 'content ' => $ blueprint ['content ' ] ]
366391 ];
392+
393+ $ post_errors = [];
394+ foreach ( $ blueprint ['content ' ] as $ contentDef ) {
395+ if ( 'posts ' === $ contentDef ['type ' ] ) {
396+ if ( isset ( $ contentDef ['source ' ] ) ) {
397+ $ sources = $ contentDef ['source ' ];
398+ } else {
399+ $ sources = $ contentDef ;
400+ }
401+
402+ if ( ! is_array ( $ sources ) ) {
403+ $ sources = [ $ sources ];
404+ }
405+
406+ foreach ( $ sources as $ source ) {
407+ $ error = $ this ->validateDataSource ( $ source , 'wp-content/content/posts/* ' );
408+ if ( $ error ) {
409+ $ post_errors [] = $ error ;
410+ }
411+ }
412+ }
413+ }
414+ if ( count ( $ post_errors ) > 0 ) {
415+ $ errors ['content ' ] = $ post_errors ;
416+ }
367417 }
368418
369419 // 14. additionalStepsAfterExecution
@@ -410,4 +460,36 @@ private function createExecutionPlan( array $blueprint ): array {
410460
411461 return [ $ plan , $ errors ];
412462 }
463+
464+ private function validateDataSource ( string $ source , string $ allowed_pattern ): ?string {
465+ if ( strlen ( $ source ) === 0 ) {
466+ return 'Source must be a non-empty string. ' ;
467+ }
468+
469+ // 1. Absolute URL.
470+ if ( str_contains ( $ source , ':// ' ) ) {
471+ return null ;
472+ }
473+
474+ // 2. Bundle-relative path.
475+ $ byte_1 = $ source [0 ];
476+ $ byte_2 = $ source [1 ] ?? null ;
477+ if ( str_contains ( $ source , '/ ' ) ) {
478+ if ( $ byte_1 === '/ ' ) {
479+ $ source = substr ( $ source , 1 );
480+ } elseif ( $ byte_1 === '. ' && $ byte_2 === '/ ' ) {
481+ $ source = substr ( $ source , 2 );
482+ }
483+
484+ if ( ! fnmatch ( $ allowed_pattern , $ source ) ) {
485+ return sprintf (
486+ 'Invalid path "%s". Expected to match "%s". ' ,
487+ $ source ,
488+ $ allowed_pattern
489+ );
490+ }
491+ }
492+
493+ return null ;
494+ }
413495}
0 commit comments