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
2 changes: 1 addition & 1 deletion docs/checks.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
| plugin_content | plugin_repo | Detects content that does not comply with the WordPress.org plugin guidelines. | [Learn more](https://developer.wordpress.org/plugins/wordpress-org/detailed-plugin-guidelines/) |
| direct_file_access | security, plugin_repo | Checks that plugin files include proper security validation using the ABSPATH constant to prevent direct file access. | [Learn more](https://developer.wordpress.org/plugins/plugin-basics/best-practices/#file-security) |
| file_type | plugin_repo | Detects the usage of hidden and compressed files, VCS directories, application files, badly named files, AI development directories (.cursor, .claude, .aider, .continue, .windsurf, .ai, .github), and unexpected markdown files in plugin root. | [Learn more](https://developer.wordpress.org/plugins/wordpress-org/detailed-plugin-guidelines/) |
| plugin_header_fields | plugin_repo | Checks adherence to the Headers requirements, including validation of "Tested up to" header matching between plugin file and readme.txt. | [Learn more](https://developer.wordpress.org/plugins/plugin-basics/header-requirements/) |
| plugin_header_fields | plugin_repo | Checks adherence to the Headers requirements. | [Learn more](https://developer.wordpress.org/plugins/plugin-basics/header-requirements/) |
| late_escaping | security, plugin_repo | Checks that all output is escaped before being sent to the browser. | [Learn more](https://developer.wordpress.org/apis/security/escaping/) |
| safe_redirect | security, plugin_repo | Checks that redirects use wp_safe_redirect() instead of wp_redirect() for security. | [Learn more](https://developer.wordpress.org/reference/functions/wp_safe_redirect/) |
| plugin_updater | plugin_repo | Prevents altering WordPress update routines or using custom updaters, which are not allowed on WordPress.org. | [Learn more](https://developer.wordpress.org/plugins/wordpress-org/detailed-plugin-guidelines/) |
Expand Down
72 changes: 42 additions & 30 deletions includes/Checker/Checks/Plugin_Repo/Plugin_Readme_Check.php
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,8 @@ protected function check_files( Check_Result $result, array $files ) {
// Check the readme for language.
$this->check_language( $result, $readme_file, $parser );

// Check for mismatched "Tested up to" header between plugin header and readme.
$this->check_tested_up_to_mismatch( $result, $parser, $result->plugin()->main_file() );
// Check for a "Tested up to" header declared in the main plugin file.
$this->check_tested_up_to_in_header( $result, $parser, $result->plugin()->main_file() );
}

/**
Expand Down Expand Up @@ -980,28 +980,26 @@ private function check_language( Check_Result $result, string $readme_file, $par


/**
* Checks for mismatched "Tested up to" header between plugin header and readme.
* Checks for a "Tested up to" header declared in the main plugin file.
*
* The "Tested up to" field is readme metadata and must be declared
* exclusively in the readme file. This reports an error whenever
* the main plugin file also declares it, whether the readme value is
* missing, equal, or different.
*
* @since 1.8.0
*
* @param Check_Result $result The Check Result to amend.
* @param DotorgParser|PCPParser $parser The Parser object.
* @param string $plugin_main_file The main plugin file path.
*/
private function check_tested_up_to_mismatch( Check_Result $result, $parser, string $plugin_main_file ) {
private function check_tested_up_to_in_header( Check_Result $result, $parser, string $plugin_main_file ) {

// Check if single file plugin, then bail early.
if ( $result->plugin()->is_single_file_plugin() ) {
return;
}

// Get the "Tested up to" value from the readme.
$readme_tested = isset( $parser->tested ) ? $parser->tested : '';

if ( empty( $readme_tested ) ) {
return;
}

// Get the "Tested up to" value from the plugin header.
$tested_header = get_file_data(
$plugin_main_file,
Expand All @@ -1014,28 +1012,42 @@ private function check_tested_up_to_mismatch( Check_Result $result, $parser, str
return;
}

// Normalize versions by removing any suffixes (like -RC1, -beta1).
$readme_tested_normalized = strtok( $readme_tested, '-' );
$plugin_tested_normalized = strtok( $plugin_tested, '-' );
// Get the "Tested up to" value from the readme, if available.
$readme_tested = isset( $parser->tested ) ? $parser->tested : '';

// Compare the two values.
if ( $readme_tested_normalized !== $plugin_tested_normalized ) {
$this->add_result_error_for_file(
$result,
sprintf(
/* translators: 1: Tested up to value from readme, 2: Tested up to value from plugin header */
__( '<strong>Mismatched "Tested up to": %1$s != %2$s.</strong><br>The "Tested up to" value in the readme file must match the "Tested up to" value in the plugin header. If the plugin header has a "Tested up to" value, it will override the readme value, which can cause confusion.', 'plugin-check' ),
esc_html( $readme_tested_normalized ),
if ( ! empty( $readme_tested ) ) {
// Normalize versions by removing any suffixes (like -RC1, -beta1).
$readme_tested_normalized = strtok( $readme_tested, '-' );
$plugin_tested_normalized = strtok( $plugin_tested, '-' );

if ( $readme_tested_normalized !== $plugin_tested_normalized ) {
$message = sprintf(
/* translators: 1: Tested up to value from plugin header, 2: Tested up to value from readme */
__( '<strong>The "Tested up to" field must be declared only in the readme file: %1$s != %2$s.</strong><br>Remove the "Tested up to" line from the main plugin PHP header and keep it only in the readme file.', 'plugin-check' ),
esc_html( $plugin_tested_normalized ),
esc_html( $readme_tested_normalized )
);
} else {
$message = sprintf(
/* translators: %s: Tested up to value */
__( '<strong>The "Tested up to" field must be declared only in the readme file (currently: %s).</strong><br>Keeping the value in both places can cause them to silently get out of sync in a future release. Remove the "Tested up to" line from the main plugin PHP header and keep it only in the readme file.', 'plugin-check' ),
esc_html( $plugin_tested_normalized )
),
'mismatched_tested_up_to_header',
$plugin_main_file,
0,
0,
'https://developer.wordpress.org/plugins/wordpress-org/how-your-readme-txt-works/#readme-header-information',
7
);
);
}
} else {
$message = __( '<strong>The "Tested up to" field must be declared only in the readme file.</strong><br>Remove the "Tested up to" line from the main plugin PHP header and keep it only in the readme file.', 'plugin-check' );
}

$this->add_result_error_for_file(
$result,
$message,
'plugin_header_tested_up_to_not_allowed',
$plugin_main_file,
0,
0,
'https://developer.wordpress.org/plugins/wordpress-org/how-your-readme-txt-works/#readme-header-information',
7
);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ public function test_run_with_mismatched_tested_up_to() {

// The "Tested up to" mismatch check has been moved to Plugin_Readme_Check.
// This test now verifies that Plugin_Header_Fields_Check does NOT report this error.
$error_items = wp_list_filter( $errors['load.php'][0][0] ?? array(), array( 'code' => 'mismatched_tested_up_to_header' ) );
$error_items = wp_list_filter( $errors['load.php'][0][0] ?? array(), array( 'code' => 'plugin_header_tested_up_to_not_allowed' ) );
$this->assertCount( 0, $error_items );
}

Expand All @@ -258,7 +258,7 @@ public function test_run_with_matching_tested_up_to() {
$errors = $check_result->get_errors();

// Should not have mismatched tested up to error.
$error_items = wp_list_filter( $errors['load.php'][0][0] ?? array(), array( 'code' => 'mismatched_tested_up_to_header' ) );
$error_items = wp_list_filter( $errors['load.php'][0][0] ?? array(), array( 'code' => 'plugin_header_tested_up_to_not_allowed' ) );
$this->assertCount( 0, $error_items );
}

Expand Down
50 changes: 20 additions & 30 deletions tests/phpunit/tests/Checker/Checks/Plugin_Readme_Check_Tests.php
Original file line number Diff line number Diff line change
Expand Up @@ -767,11 +767,11 @@ public function test_run_with_mismatch() {
$this->assertNotEmpty( $errors );
$this->assertArrayHasKey( 'load.php', $errors );

// Check for mismatched "Tested up to" error.
$this->assertCount( 1, wp_list_filter( $errors['load.php'][0][0], array( 'code' => 'mismatched_tested_up_to_header' ) ) );
// Check for "Tested up to" declared in the plugin header error.
$this->assertCount( 1, wp_list_filter( $errors['load.php'][0][0], array( 'code' => 'plugin_header_tested_up_to_not_allowed' ) ) );

// Verify the error message contains the correct versions.
$error_items = wp_list_filter( $errors['load.php'][0][0], array( 'code' => 'mismatched_tested_up_to_header' ) );
$error_items = wp_list_filter( $errors['load.php'][0][0], array( 'code' => 'plugin_header_tested_up_to_not_allowed' ) );
$error_message = reset( $error_items )['message'];
$this->assertStringContainsString( '6.7', $error_message );
$this->assertStringContainsString( '6.5', $error_message );
Expand All @@ -786,18 +786,15 @@ public function test_run_with_match() {

$errors = $check_result->get_errors();

// Should not have mismatched tested up to error when values match.
// Note: Other readme errors may still be present.
if ( ! empty( $errors ) ) {
foreach ( $errors as $file => $file_errors ) {
if ( isset( $file_errors[0][0] ) ) {
$this->assertCount( 0, wp_list_filter( $file_errors[0][0], array( 'code' => 'mismatched_tested_up_to_header' ) ) );
}
}
}
$this->assertNotEmpty( $errors );
$this->assertArrayHasKey( 'load.php', $errors );

// Explicitly assert that we checked for the error code.
$this->assertTrue( true );
// Even when the values match, declaring "Tested up to" in the header must be reported.
$this->assertCount( 1, wp_list_filter( $errors['load.php'][0][0], array( 'code' => 'plugin_header_tested_up_to_not_allowed' ) ) );

$error_items = wp_list_filter( $errors['load.php'][0][0], array( 'code' => 'plugin_header_tested_up_to_not_allowed' ) );
$error_message = reset( $error_items )['message'];
$this->assertStringContainsString( '6.7', $error_message );
}

public function test_run_with_readme_only() {
Expand All @@ -809,12 +806,12 @@ public function test_run_with_readme_only() {

$errors = $check_result->get_errors();

// Should not have mismatched tested up to error when only readme has the value.
// Should not report an error when "Tested up to" is only declared in the readme.
// Note: Other readme errors may still be present.
if ( ! empty( $errors ) ) {
foreach ( $errors as $file => $file_errors ) {
if ( isset( $file_errors[0][0] ) ) {
$this->assertCount( 0, wp_list_filter( $file_errors[0][0], array( 'code' => 'mismatched_tested_up_to_header' ) ) );
$this->assertCount( 0, wp_list_filter( $file_errors[0][0], array( 'code' => 'plugin_header_tested_up_to_not_allowed' ) ) );
}
}
}
Expand All @@ -832,18 +829,11 @@ public function test_run_with_header_only() {

$errors = $check_result->get_errors();

// Should not have mismatched tested up to error when only header has the value.
// Note: Other readme errors may still be present.
if ( ! empty( $errors ) ) {
foreach ( $errors as $file => $file_errors ) {
if ( isset( $file_errors[0][0] ) ) {
$this->assertCount( 0, wp_list_filter( $file_errors[0][0], array( 'code' => 'mismatched_tested_up_to_header' ) ) );
}
}
}
$this->assertNotEmpty( $errors );
$this->assertArrayHasKey( 'load.php', $errors );

// Explicitly assert that we checked for the error code.
$this->assertTrue( true );
// "Tested up to" declared only in the header must still be reported.
$this->assertCount( 1, wp_list_filter( $errors['load.php'][0][0], array( 'code' => 'plugin_header_tested_up_to_not_allowed' ) ) );
}

public function test_run_with_single_file_plugin() {
Expand All @@ -855,12 +845,12 @@ public function test_run_with_single_file_plugin() {

$errors = $check_result->get_errors();

// Should not have mismatched tested up to errors for single-file plugins.
// Should not report "Tested up to" header errors for single-file plugins.
// Note: Other header field errors may still be present.
if ( ! empty( $errors ) ) {
foreach ( $errors as $file => $file_errors ) {
if ( isset( $file_errors[0][0] ) ) {
$this->assertCount( 0, wp_list_filter( $file_errors[0][0], array( 'code' => 'mismatched_tested_up_to_header' ) ) );
$this->assertCount( 0, wp_list_filter( $file_errors[0][0], array( 'code' => 'plugin_header_tested_up_to_not_allowed' ) ) );
}
}
}
Expand All @@ -879,6 +869,6 @@ public function test_run_with_no_readme() {
$errors = $check_result->get_errors();

// Should not have tested up to errors when readme doesn't exist.
$this->assertEmpty( wp_list_filter( $errors, array( 'code' => 'mismatched_tested_up_to_header' ) ) );
$this->assertEmpty( wp_list_filter( $errors, array( 'code' => 'plugin_header_tested_up_to_not_allowed' ) ) );
}
}
Loading