Skip to content
Open
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
124 changes: 108 additions & 16 deletions includes/Checker/Checks/Plugin_Repo/Plugin_Header_Fields_Check.php
Original file line number Diff line number Diff line change
Expand Up @@ -372,22 +372,7 @@ public function run( Check_Result $result ) {
}

if ( ! empty( $plugin_header['RequiresPlugins'] ) ) {
if ( ! preg_match( '/^[a-z0-9-]+(?:,\s*[a-z0-9-]+)*$/', $plugin_header['RequiresPlugins'] ) ) {
$this->add_result_error_for_file(
$result,
sprintf(
/* translators: %s: plugin header field */
__( 'The "%s" header in the plugin file must contain a comma-separated list of WordPress.org-formatted slugs.', 'plugin-check' ),
esc_html( $labels['RequiresPlugins'] )
),
'plugin_header_invalid_requires_plugins',
$plugin_main_file,
0,
0,
'',
7
);
}
$this->check_requires_plugins_header( $result, $plugin_header['RequiresPlugins'], $labels['RequiresPlugins'], $plugin_main_file );
}

if ( empty( $plugin_header['License'] ) ) {
Expand Down Expand Up @@ -536,6 +521,113 @@ public function run( Check_Result $result ) {
}
}

/**
* Validates each slug declared in the "Requires Plugins" header individually.
*
* Reports an error per malformed slug, and a warning per validly formatted
* slug that could not be found in the WordPress.org plugin directory.
*
* @since 2.1.0
*
* @param Check_Result $result The check result to amend.
* @param string $requires_plugins Raw value of the "Requires Plugins" header.
* @param string $label Label of the "Requires Plugins" header.
* @param string $plugin_main_file Absolute path to the main plugin file.
*/
private function check_requires_plugins_header( Check_Result $result, string $requires_plugins, string $label, string $plugin_main_file ) {
$slugs = array_map( 'trim', explode( ',', $requires_plugins ) );

foreach ( $slugs as $slug ) {
if ( '' === $slug ) {
continue;
}

if ( ! preg_match( '/^[a-z0-9]+(?:-[a-z0-9]+)*$/', $slug ) ) {
$this->add_result_error_for_file(
$result,
sprintf(
/* translators: 1: plugin header field, 2: dependency slug */
__( 'The "%1$s" header in the plugin file contains "%2$s", which is not a valid WordPress.org-formatted slug.', 'plugin-check' ),
esc_html( $label ),
esc_html( $slug )
),
'plugin_header_invalid_requires_plugins',
$plugin_main_file,
0,
0,
'',
7
);
continue;
}

$this->check_requires_plugins_slug_status( $result, $slug, $plugin_main_file );
}
}

/**
* Checks whether a single declared plugin dependency exists in the WordPress.org plugin directory.
*
* @since 2.1.0
*
* @param Check_Result $result The check result to amend.
* @param string $slug The dependency's WordPress.org slug.
* @param string $plugin_main_file Absolute path to the main plugin file.
*/
private function check_requires_plugins_slug_status( Check_Result $result, string $slug, string $plugin_main_file ) {
$exists = $this->plugin_exists_in_directory( $slug );

// Unknown due to an unreachable API; skip rather than risk a false positive.
if ( null === $exists || $exists ) {
return;
}

$this->add_result_warning_for_file(
$result,
sprintf(
/* translators: %s: dependency slug */
__( 'The plugin declares "%s" as a required plugin, but it could not be found in the WordPress.org plugin directory.', 'plugin-check' ),
esc_html( $slug )
),
'plugin_header_requires_plugins_not_in_directory',
$plugin_main_file,
0,
0,
'',
3
);
}

/**
* Checks whether a plugin slug exists in the WordPress.org plugin directory.
*
* @since 2.1.0
*
* @param string $slug The plugin's WordPress.org slug.
* @return bool|null True if found, false if not found, null if the API could not be reached.
*/
private function plugin_exists_in_directory( string $slug ) {
$transient_key = 'wp_plugin_check_requires_plugin_' . $slug;
$exists = get_transient( $transient_key );

if ( false !== $exists ) {
return 'yes' === $exists;
}

$response = wp_remote_get( "https://api.wordpress.org/plugins/info/1.0/{$slug}.json" );

if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
return null;
}

$body = json_decode( wp_remote_retrieve_body( $response ), true );
$exists = isset( $body['slug'] ) && ! isset( $body['error'] );

set_transient( $transient_key, $exists ? 'yes' : 'no', DAY_IN_SECONDS );

return $exists;
}

/**
* Parses the plugin contents to retrieve plugin's metadata.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php
/**
* Plugin Name: Test Plugin Requires Plugins Status
* Plugin URI: https://github.com/WordPress/plugin-check
* Description: Test plugin for the Requires Plugins dependency status check.
* Requires at least: 6.0
* Requires PHP: 7.4
* Version: 1.0.0
* Author: WordPress Performance Team
* Author URI: https://make.wordpress.org/performance/
* License: GPLv2 or later
* License URI: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
* Text Domain: test-plugin-requires-plugins-status
* Requires Plugins: plugin-check, hello-dolly, not-installed-plugin
*
* @package test-plugin-requires-plugins-status
*/
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ public function test_run_with_errors() {
$this->assertCount( 1, wp_list_filter( $warnings['load.php'][0][0], array( 'code' => 'plugin_header_nonexistent_domain_path' ) ) );

if ( is_wp_version_compatible( '6.5' ) ) {
$this->assertCount( 1, wp_list_filter( $errors['load.php'][0][0], array( 'code' => 'plugin_header_invalid_requires_plugins' ) ) );
// Fixture declares "Example Plugin, OtherPlugin", neither of which is a valid slug.
$this->assertCount( 2, wp_list_filter( $errors['load.php'][0][0], array( 'code' => 'plugin_header_invalid_requires_plugins' ) ) );
}
}

Expand Down Expand Up @@ -89,6 +90,9 @@ public function test_run_with_valid_requires_plugins_header() {
* Requires Plugins: woocommerce, contact-form-7
*/

set_transient( 'wp_plugin_check_requires_plugin_woocommerce', 'yes' );
set_transient( 'wp_plugin_check_requires_plugin_contact-form-7', 'yes' );

$check = new Plugin_Header_Fields_Check();
$check_context = new Check_Context( UNIT_TESTS_PLUGIN_DIR . 'test-plugin-unfiltered-uploads-with-errors/load.php' );
$check_result = new Check_Result( $check_context );
Expand All @@ -97,6 +101,9 @@ public function test_run_with_valid_requires_plugins_header() {

$errors = $check_result->get_errors();

delete_transient( 'wp_plugin_check_requires_plugin_woocommerce' );
delete_transient( 'wp_plugin_check_requires_plugin_contact-form-7' );

if ( is_wp_version_compatible( '6.5' ) ) {
$this->assertCount( 0, wp_list_filter( $errors['load.php'][0][0], array( 'code' => 'plugin_header_invalid_requires_plugins' ) ) );
} else {
Expand All @@ -105,6 +112,88 @@ public function test_run_with_valid_requires_plugins_header() {
}
}

public function test_run_with_multiple_invalid_requires_plugins_slugs_reported_individually() {
/*
* Test plugin has following header, with both slugs invalid.
* Requires Plugins: Example Plugin, OtherPlugin
*/

$check = new Plugin_Header_Fields_Check();
$check_context = new Check_Context( UNIT_TESTS_PLUGIN_DIR . 'test-plugin-header-fields-with-errors/load.php' );
$check_result = new Check_Result( $check_context );

$check->run( $check_result );

$errors = $check_result->get_errors();

if ( ! is_wp_version_compatible( '6.5' ) ) {
$this->assertTrue( true );
return;
}

$messages = wp_list_pluck( wp_list_filter( $errors['load.php'][0][0], array( 'code' => 'plugin_header_invalid_requires_plugins' ) ), 'message' );

$this->assertCount( 2, $messages, 'Each invalid slug should be reported as its own error.' );

$example_plugin_messages = array_filter(
$messages,
static function ( $message ) {
return str_contains( $message, 'Example Plugin' );
}
);
$other_plugin_messages = array_filter(
$messages,
static function ( $message ) {
return str_contains( $message, 'OtherPlugin' );
}
);

$this->assertCount( 1, $example_plugin_messages, 'Exactly one error should name "Example Plugin".' );
$this->assertStringNotContainsString( 'OtherPlugin', reset( $example_plugin_messages ) );

$this->assertCount( 1, $other_plugin_messages, 'Exactly one error should name "OtherPlugin".' );
$this->assertStringNotContainsString( 'Example Plugin', reset( $other_plugin_messages ) );
}

public function test_run_with_requires_plugins_dependency_status() {
/*
* Test plugin has following header.
* Requires Plugins: plugin-check, hello-dolly, not-installed-plugin
*
* "plugin-check" and "hello-dolly" are seeded as published in the
* WordPress.org plugin directory; "not-installed-plugin" is seeded
* as not found there.
*/

set_transient( 'wp_plugin_check_requires_plugin_plugin-check', 'yes' );
set_transient( 'wp_plugin_check_requires_plugin_hello-dolly', 'yes' );
set_transient( 'wp_plugin_check_requires_plugin_not-installed-plugin', 'no' );

$check = new Plugin_Header_Fields_Check();
$check_context = new Check_Context( UNIT_TESTS_PLUGIN_DIR . 'test-plugin-requires-plugins-status/load.php' );
$check_result = new Check_Result( $check_context );

$check->run( $check_result );

$errors = $check_result->get_errors();
$warnings = $check_result->get_warnings();

delete_transient( 'wp_plugin_check_requires_plugin_plugin-check' );
delete_transient( 'wp_plugin_check_requires_plugin_hello-dolly' );
delete_transient( 'wp_plugin_check_requires_plugin_not-installed-plugin' );

if ( ! is_wp_version_compatible( '6.5' ) ) {
$this->assertTrue( true );
return;
}

$this->assertCount( 0, wp_list_filter( $errors['load.php'][0][0] ?? array(), array( 'code' => 'plugin_header_invalid_requires_plugins' ) ) );

$not_in_directory_items = wp_list_filter( $warnings['load.php'][0][0], array( 'code' => 'plugin_header_requires_plugins_not_in_directory' ) );
$this->assertCount( 1, $not_in_directory_items, 'Only the dependency missing from the directory should get a warning.' );
$this->assertStringContainsString( 'not-installed-plugin', reset( $not_in_directory_items )['message'] );
}

public function test_run_with_mismatched_tested_up_to() {
$check = new Plugin_Header_Fields_Check();
$check_context = new Check_Context( UNIT_TESTS_PLUGIN_DIR . 'test-plugin-tested-up-to-mismatch/load.php' );
Expand Down
Loading