@@ -417,7 +417,14 @@ private function buildPluginStep( $plugin ): array {
417417 $ plugin = [ 'source ' => $ plugin ];
418418 }
419419
420- $ error = $ this ->validateDataSource ( $ plugin ['source ' ], 'wp-content/plugins/* ' );
420+ $ source = $ plugin ['source ' ];
421+ $ error = $ this ->validateDataSource ( $ source , 'wp-content/plugins/* ' );
422+ if ( null === $ error ) {
423+ if ( str_ends_with ( $ source , '.php ' ) ) {
424+ $ error = $ this ->validatePluginFile ( $ source );
425+ }
426+ }
427+
421428 return [
422429 'key ' => 'plugins ' ,
423430 'name ' => 'installPlugin ' ,
@@ -554,6 +561,43 @@ private function validateDataSource( string $source, string $allowed_pattern ):
554561 return null ;
555562 }
556563
564+ /**
565+ * Validate a plugin file as per WordPress plugin requirements.
566+ *
567+ * The implementation mirrors "get_file_data()" from WordPress core.
568+ *
569+ * @param string $source The path to the plugin file.
570+ * @return string|null An error message if the plugin file is invalid, or null if it is valid.
571+ */
572+ private function validatePluginFile ( string $ source ): ?string {
573+ $ root = $ this ->configuration ->getTargetSiteRoot ();
574+ $ file = rtrim ( $ root , '/ ' ) . ltrim ( $ source , '. ' );
575+
576+ // Pull only the first 8 KB of the file in.
577+ $ file_data = file_get_contents ( $ file , false , null , 0 , 8 * 1024 );
578+ if ( false === $ file_data ) {
579+ $ file_data = '' ;
580+ }
581+
582+ // Make sure we catch CR-only line endings.
583+ $ file_data = str_replace ( "\r" , "\n" , $ file_data );
584+
585+ // We only need to check for the plugin name header.
586+ $ all_headers = array ( 'Name ' => 'Plugin Name ' );
587+ foreach ( $ all_headers as $ field => $ regex ) {
588+ if ( preg_match ( '/^(?:[ \t]*<\?php)?[ \t\/*#@]* ' . preg_quote ( $ regex , '/ ' ) . ':(.*)$/mi ' , $ file_data , $ match ) && $ match [1 ] ) {
589+ $ all_headers [ $ field ] = $ match [1 ];
590+ } else {
591+ $ all_headers [ $ field ] = '' ;
592+ }
593+ }
594+
595+ if ( empty ( $ all_headers ['Name ' ] ) ) {
596+ return 'Invalid plugin file. Missing "Plugin Name" header. ' ;
597+ }
598+ return null ;
599+ }
600+
557601 /**
558602 * Detect on which line a top-level JSON key is defined in the input string.
559603 * This is a simple helper that only supports top-level keys in a top-level
0 commit comments