diff --git a/.github/workflows/behat-test.yml b/.github/workflows/behat-test.yml
index 85e664fb6..69346749f 100644
--- a/.github/workflows/behat-test.yml
+++ b/.github/workflows/behat-test.yml
@@ -155,7 +155,7 @@ jobs:
- name: Upload code coverage report
if: ${{ matrix.coverage }}
- uses: codecov/codecov-action@v4.6.0
+ uses: codecov/codecov-action@v5.0.7
with:
files: ${{ steps.coverage_files.outputs.files }}
flags: feature
diff --git a/.github/workflows/php-lint.yml b/.github/workflows/php-lint.yml
index 924378b15..4087b95cd 100644
--- a/.github/workflows/php-lint.yml
+++ b/.github/workflows/php-lint.yml
@@ -61,3 +61,32 @@ jobs:
- name: PHPMD
run: composer phpmd
+
+ php-lint-sniffs:
+ name: PHP (Sniffs)
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: shivammathur/setup-php@v2
+ with:
+ php-version: '8.0'
+
+ - name: Validate Composer configuration
+ working-directory: "phpcs-sniffs"
+ run: composer validate
+
+ - name: Install PHP dependencies
+ uses: ramsey/composer-install@57532f8be5bda426838819c5ee9afb8af389d51a
+ with:
+ composer-options: '--prefer-dist'
+ working-directory: "phpcs-sniffs"
+
+ - name: PHP Lint
+ working-directory: "phpcs-sniffs"
+ run: composer lint
+
+ - name: PHP Lint PHPCS
+ working-directory: "phpcs-sniffs"
+ run: composer check-cs
diff --git a/.github/workflows/php-test.yml b/.github/workflows/php-test.yml
index 03bc69be9..eb53359a0 100644
--- a/.github/workflows/php-test.yml
+++ b/.github/workflows/php-test.yml
@@ -100,7 +100,7 @@ jobs:
- name: Upload code coverage report
if: ${{ matrix.coverage }}
- uses: codecov/codecov-action@68708a9f7a6b6b5fe33673f782f93725c5eff3c6
+ uses: codecov/codecov-action@015f24e6818733317a2da2edd6290ab26238649a
with:
file: build/logs/*.xml
flags: unit
@@ -146,7 +146,7 @@ jobs:
- name: Upload code coverage report
if: ${{ matrix.coverage }}
- uses: codecov/codecov-action@68708a9f7a6b6b5fe33673f782f93725c5eff3c6
+ uses: codecov/codecov-action@015f24e6818733317a2da2edd6290ab26238649a
with:
file: build/logs/*.xml
flags: phpcs-sniffs
diff --git a/includes/Checker/Checks/Plugin_Repo/Plugin_Header_Fields_Check.php b/includes/Checker/Checks/Plugin_Repo/Plugin_Header_Fields_Check.php
index f184f251f..591683ba4 100644
--- a/includes/Checker/Checks/Plugin_Repo/Plugin_Header_Fields_Check.php
+++ b/includes/Checker/Checks/Plugin_Repo/Plugin_Header_Fields_Check.php
@@ -308,7 +308,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_warning_for_file(
+ $this->add_result_error_for_file(
$result,
sprintf(
/* translators: %s: plugin header field */
@@ -320,7 +320,7 @@ public function run( Check_Result $result ) {
0,
0,
'',
- 6
+ 7
);
}
}
diff --git a/includes/Checker/Checks/Plugin_Repo/Plugin_Readme_Check.php b/includes/Checker/Checks/Plugin_Repo/Plugin_Readme_Check.php
index ba9c7b859..ab949b49f 100644
--- a/includes/Checker/Checks/Plugin_Repo/Plugin_Readme_Check.php
+++ b/includes/Checker/Checks/Plugin_Repo/Plugin_Readme_Check.php
@@ -104,6 +104,9 @@ protected function check_files( Check_Result $result, array $files ) {
// Check the readme file for warnings.
$this->check_for_warnings( $result, $readme_file, $parser );
+ // Check the readme file for donate link.
+ $this->check_for_donate_link( $result, $readme_file, $parser );
+
// Check the readme file for contributors.
$this->check_for_contributors( $result, $readme_file );
}
@@ -601,6 +604,41 @@ private function check_for_warnings( Check_Result $result, string $readme_file,
}
}
+ /**
+ * Checks the readme file for donate link.
+ *
+ * @since 1.3.0
+ *
+ * @param Check_Result $result The Check Result to amend.
+ * @param string $readme_file Readme file.
+ * @param Parser $parser The Parser object.
+ */
+ private function check_for_donate_link( Check_Result $result, string $readme_file, Parser $parser ) {
+ $donate_link = $parser->donate_link;
+
+ // Bail if empty donate link.
+ if ( empty( $donate_link ) ) {
+ return;
+ }
+
+ if ( ! ( filter_var( $donate_link, FILTER_VALIDATE_URL ) === $donate_link && str_starts_with( $donate_link, 'http' ) ) ) {
+ $this->add_result_warning_for_file(
+ $result,
+ sprintf(
+ /* translators: %s: plugin header field */
+ __( 'The "%s" header in the readme file must be a valid URL.', 'plugin-check' ),
+ 'Donate link'
+ ),
+ 'readme_invalid_donate_link',
+ $readme_file,
+ 0,
+ 0,
+ 'https://developer.wordpress.org/plugins/wordpress-org/how-your-readme-txt-works/#readme-header-information',
+ 6
+ );
+ }
+ }
+
/**
* Checks the readme file for contributors.
*
diff --git a/includes/Checker/Checks/Security/Late_Escaping_Check.php b/includes/Checker/Checks/Security/Late_Escaping_Check.php
index c7a7df995..61eab021e 100644
--- a/includes/Checker/Checks/Security/Late_Escaping_Check.php
+++ b/includes/Checker/Checks/Security/Late_Escaping_Check.php
@@ -78,4 +78,41 @@ public function get_description(): string {
public function get_documentation_url(): string {
return __( 'https://developer.wordpress.org/apis/security/escaping/', 'plugin-check' );
}
+
+ /**
+ * Amends the given result with a message for the specified file, including error information.
+ *
+ * @since 1.3.0
+ *
+ * @param Check_Result $result The check result to amend, including the plugin context to check.
+ * @param bool $error Whether it is an error or notice.
+ * @param string $message Error message.
+ * @param string $code Error code.
+ * @param string $file Absolute path to the file where the issue was found.
+ * @param int $line The line on which the message occurred. Default is 0 (unknown line).
+ * @param int $column The column on which the message occurred. Default is 0 (unknown column).
+ * @param string $docs URL for further information about the message.
+ * @param int $severity Severity level. Default is 5.
+ */
+ protected function add_result_message_for_file( Check_Result $result, $error, $message, $code, $file, $line = 0, $column = 0, string $docs = '', $severity = 5 ) {
+ switch ( $code ) {
+ case 'WordPress.Security.EscapeOutput.OutputNotEscaped':
+ $docs = __( 'https://developer.wordpress.org/apis/security/escaping/#escaping-functions', 'plugin-check' );
+ break;
+
+ case 'WordPress.Security.EscapeOutput.UnsafePrintingFunction':
+ $docs = __( 'https://developer.wordpress.org/apis/security/escaping/#escaping-with-localization', 'plugin-check' );
+ break;
+
+ case 'WordPress.Security.EscapeOutput.UnsafeSearchQuery':
+ $docs = __( 'https://developer.wordpress.org/reference/functions/get_search_query/', 'plugin-check' );
+ break;
+
+ default:
+ $docs = __( 'https://developer.wordpress.org/apis/security/escaping/', 'plugin-check' );
+ break;
+ }
+
+ parent::add_result_message_for_file( $result, $error, $message, $code, $file, $line, $column, $docs, $severity );
+ }
}
diff --git a/package-lock.json b/package-lock.json
index 46c89297f..d32c52aa6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,7 +8,7 @@
"hasInstallScript": true,
"license": "GPL-2.0-or-later",
"devDependencies": {
- "@wordpress/env": "^10.11.0",
+ "@wordpress/env": "^10.12.0",
"@wordpress/scripts": "^30.5.1",
"gherkin-lint": "^4.2.4",
"patch-package": "^8.0.0"
@@ -4448,10 +4448,11 @@
}
},
"node_modules/@wordpress/env": {
- "version": "10.11.0",
- "resolved": "https://registry.npmjs.org/@wordpress/env/-/env-10.11.0.tgz",
- "integrity": "sha512-Sd31oiYxy9/pNMfYw7XgtCBoD4PpvcK1UQ/FVFP+DsTTuo55Ma4UAHMKfg2NhnZWqhJTgPC6XXzicGpr/lK4RQ==",
+ "version": "10.12.0",
+ "resolved": "https://registry.npmjs.org/@wordpress/env/-/env-10.12.0.tgz",
+ "integrity": "sha512-+tsdVfngQYcysxdVonXRSwuJjqoqTSv7wwrcThCYXR1OBCMQ/xT2Ywfvf9a/yItJs5uicO9Vx8B5aIuvXiGVqg==",
"dev": true,
+ "license": "GPL-2.0-or-later",
"dependencies": {
"chalk": "^4.0.0",
"copy-dir": "^1.3.0",
@@ -22905,9 +22906,9 @@
}
},
"@wordpress/env": {
- "version": "10.11.0",
- "resolved": "https://registry.npmjs.org/@wordpress/env/-/env-10.11.0.tgz",
- "integrity": "sha512-Sd31oiYxy9/pNMfYw7XgtCBoD4PpvcK1UQ/FVFP+DsTTuo55Ma4UAHMKfg2NhnZWqhJTgPC6XXzicGpr/lK4RQ==",
+ "version": "10.12.0",
+ "resolved": "https://registry.npmjs.org/@wordpress/env/-/env-10.12.0.tgz",
+ "integrity": "sha512-+tsdVfngQYcysxdVonXRSwuJjqoqTSv7wwrcThCYXR1OBCMQ/xT2Ywfvf9a/yItJs5uicO9Vx8B5aIuvXiGVqg==",
"dev": true,
"requires": {
"chalk": "^4.0.0",
diff --git a/package.json b/package.json
index 6d99f1ee2..4e7460cfb 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,7 @@
"npm": ">=10.2.3"
},
"devDependencies": {
- "@wordpress/env": "^10.11.0",
+ "@wordpress/env": "^10.12.0",
"@wordpress/scripts": "^30.5.1",
"gherkin-lint": "^4.2.4",
"patch-package": "^8.0.0"
diff --git a/patches/@wordpress+env+10.10.0.patch b/patches/@wordpress+env+10.12.0.patch
similarity index 100%
rename from patches/@wordpress+env+10.10.0.patch
rename to patches/@wordpress+env+10.12.0.patch
diff --git a/phpcs-rulesets/plugin-review.xml b/phpcs-rulesets/plugin-review.xml
index b4d5eedec..c517bab3a 100644
--- a/phpcs-rulesets/plugin-review.xml
+++ b/phpcs-rulesets/plugin-review.xml
@@ -56,6 +56,11 @@
7
+
+
+ 7
+
+
error
@@ -141,7 +146,7 @@
7
-
+
6
@@ -151,4 +156,9 @@
7
+
+
+ 7
+
+
diff --git a/phpcs-sniffs/PluginCheck/Sniffs/CodeAnalysis/LocalhostSniff.php b/phpcs-sniffs/PluginCheck/Sniffs/CodeAnalysis/LocalhostSniff.php
index fb28251f9..63d54d11f 100644
--- a/phpcs-sniffs/PluginCheck/Sniffs/CodeAnalysis/LocalhostSniff.php
+++ b/phpcs-sniffs/PluginCheck/Sniffs/CodeAnalysis/LocalhostSniff.php
@@ -55,7 +55,7 @@ public function process_token( $stackPtr ) {
'Do not use Localhost/127.0.0.1 in your code. Found: %s',
$this->find_token_in_multiline_string( $stackPtr, $content, $match[1] ),
'Found',
- [ $match[0] ]
+ array( $match[0] )
);
}
}
diff --git a/phpcs-sniffs/PluginCheck/Sniffs/CodeAnalysis/RequiredFunctionParametersSniff.php b/phpcs-sniffs/PluginCheck/Sniffs/CodeAnalysis/RequiredFunctionParametersSniff.php
new file mode 100644
index 000000000..f81addfa9
--- /dev/null
+++ b/phpcs-sniffs/PluginCheck/Sniffs/CodeAnalysis/RequiredFunctionParametersSniff.php
@@ -0,0 +1,84 @@
+> Function name as key, array with target parameter and name as value.
+ */
+ protected $target_functions = array(
+ 'parse_str' => array(
+ 'position' => 2,
+ 'name' => 'result',
+ ),
+ );
+
+ /**
+ * Processes this test, when one of its tokens is encountered.
+ *
+ * @since 1.3.0
+ *
+ * @param int $stackPtr The position of the current token in the stack.
+ * @return int|void Integer stack pointer to skip forward or void to continue normal file processing.
+ */
+ public function process_token( $stackPtr ) {
+ if ( isset( $this->target_functions[ strtolower( $this->tokens[ $stackPtr ]['content'] ) ] ) ) {
+ // Disallow excluding function groups for this sniff.
+ $this->exclude = array();
+
+ return parent::process_token( $stackPtr );
+ }
+ }
+
+ /**
+ * Process the parameters of a matched function call.
+ *
+ * @since 1.3.0
+ *
+ * @param int $stackPtr The position of the current token in the stack.
+ * @param string $group_name The name of the group which was matched.
+ * @param string $matched_content The token content (function name) which was matched in lowercase.
+ * @param array $parameters Array with information about the parameters.
+ * @return void
+ */
+ public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) {
+ $target_param = $this->target_functions[ $matched_content ];
+
+ $found_param = PassedParameters::getParameterFromStack( $parameters, $target_param['position'], $target_param['name'] );
+
+ if ( false === $found_param ) {
+ $error_code = MessageHelper::stringToErrorCode( $matched_content . '_' . $target_param['name'], true );
+
+ $this->phpcsFile->addError(
+ 'The "%s" parameter for function %s() is missing.',
+ $stackPtr,
+ $error_code . 'Missing',
+ array( $target_param['name'], $matched_content )
+ );
+ }
+ }
+}
diff --git a/phpcs-sniffs/PluginCheck/Tests/CodeAnalysis/RequiredFunctionParametersUnitTest.inc b/phpcs-sniffs/PluginCheck/Tests/CodeAnalysis/RequiredFunctionParametersUnitTest.inc
new file mode 100644
index 000000000..a115ffa0c
--- /dev/null
+++ b/phpcs-sniffs/PluginCheck/Tests/CodeAnalysis/RequiredFunctionParametersUnitTest.inc
@@ -0,0 +1,11 @@
+ =>
+ */
+ public function getErrorList() {
+ return array(
+ 4 => 1,
+ 8 => 1,
+ 11 => 1,
+ );
+ }
+
+ /**
+ * Returns the lines where warnings should occur.
+ *
+ * @return array =>
+ */
+ public function getWarningList() {
+ return array();
+ }
+
+ /**
+ * Returns the fully qualified class name (FQCN) of the sniff.
+ *
+ * @return string The fully qualified class name of the sniff.
+ */
+ protected function get_sniff_fqcn() {
+ return RequiredFunctionParametersSniff::class;
+ }
+
+ /**
+ * Sets the parameters for the sniff.
+ *
+ * @throws \RuntimeException If unable to set the ruleset parameters required for the test.
+ *
+ * @param Sniff $sniff The sniff being tested.
+ */
+ public function set_sniff_parameters( Sniff $sniff ) {
+ }
+}
diff --git a/phpcs-sniffs/PluginCheck/ruleset.xml b/phpcs-sniffs/PluginCheck/ruleset.xml
index e07fe0d20..4243c3285 100644
--- a/phpcs-sniffs/PluginCheck/ruleset.xml
+++ b/phpcs-sniffs/PluginCheck/ruleset.xml
@@ -7,5 +7,6 @@
+
diff --git a/tests/phpunit/testdata/plugins/test-plugin-plugin-readme-md-with-errors/readme.md b/tests/phpunit/testdata/plugins/test-plugin-plugin-readme-md-with-errors/readme.md
index a6e6ce7e9..68637e562 100644
--- a/tests/phpunit/testdata/plugins/test-plugin-plugin-readme-md-with-errors/readme.md
+++ b/tests/phpunit/testdata/plugins/test-plugin-plugin-readme-md-with-errors/readme.md
@@ -9,5 +9,6 @@ Stable tag: trunk
License: Oculus VR Inc. Software Development Kit License
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Tags: performance, testing, security
+Donate link: not-a-valid-url
Here is a short description of the plugin.
diff --git a/tests/phpunit/testdata/plugins/test-plugin-review-phpcs-errors/load.php b/tests/phpunit/testdata/plugins/test-plugin-review-phpcs-errors/load.php
index 96e8669a5..9e59cf503 100644
--- a/tests/phpunit/testdata/plugins/test-plugin-review-phpcs-errors/load.php
+++ b/tests/phpunit/testdata/plugins/test-plugin-review-phpcs-errors/load.php
@@ -24,3 +24,12 @@
query_posts( 'cat=3' );
wp_reset_query();
+
+$str = <<assertCount( 1, wp_list_filter( $warnings['load.php'][0][0], array( 'code' => 'plugin_header_invalid_network' ) ) );
if ( is_wp_version_compatible( '6.5' ) ) {
- $this->assertCount( 1, wp_list_filter( $warnings['load.php'][0][0], array( 'code' => 'plugin_header_invalid_requires_plugins' ) ) );
+ $this->assertCount( 1, wp_list_filter( $errors['load.php'][0][0], array( 'code' => 'plugin_header_invalid_requires_plugins' ) ) );
}
}
@@ -53,10 +53,10 @@ public function test_run_with_valid_requires_plugins_header() {
$check->run( $check_result );
- $warnings = $check_result->get_warnings();
+ $errors = $check_result->get_errors();
if ( is_wp_version_compatible( '6.5' ) ) {
- $this->assertCount( 0, wp_list_filter( $warnings['load.php'][0][0], array( 'code' => 'plugin_header_invalid_requires_plugins' ) ) );
+ $this->assertCount( 0, wp_list_filter( $errors['load.php'][0][0], array( 'code' => 'plugin_header_invalid_requires_plugins' ) ) );
}
}
diff --git a/tests/phpunit/tests/Checker/Checks/Plugin_Readme_Check_Tests.php b/tests/phpunit/tests/Checker/Checks/Plugin_Readme_Check_Tests.php
index 275da69c0..a478c22f6 100644
--- a/tests/phpunit/tests/Checker/Checks/Plugin_Readme_Check_Tests.php
+++ b/tests/phpunit/tests/Checker/Checks/Plugin_Readme_Check_Tests.php
@@ -234,6 +234,7 @@ public function test_run_md_with_errors() {
$this->assertCount( 1, wp_list_filter( $warnings['readme.md'][0][0], array( 'code' => 'mismatched_plugin_name' ) ) );
$this->assertCount( 1, wp_list_filter( $warnings['readme.md'][0][0], array( 'code' => 'readme_invalid_contributors' ) ) );
+ $this->assertCount( 1, wp_list_filter( $warnings['readme.md'][0][0], array( 'code' => 'readme_invalid_donate_link' ) ) );
}
public function test_single_file_plugin_without_error_for_trademarks() {
diff --git a/tests/phpunit/tests/Checker/Checks/Plugin_Review_PHPCS_Check_Tests.php b/tests/phpunit/tests/Checker/Checks/Plugin_Review_PHPCS_Check_Tests.php
index 29171a712..22e67c78b 100644
--- a/tests/phpunit/tests/Checker/Checks/Plugin_Review_PHPCS_Check_Tests.php
+++ b/tests/phpunit/tests/Checker/Checks/Plugin_Review_PHPCS_Check_Tests.php
@@ -32,12 +32,18 @@ public function test_run_with_errors() {
$this->assertArrayHasKey( 'code', $errors['load.php'][6][1][0] );
$this->assertEquals( 'Generic.PHP.DisallowShortOpenTag.Found', $errors['load.php'][6][1][0]['code'] );
+ // Check for Squiz.PHP.Heredoc.NotAllowed error on Line no 28 and column no at 8.
+ $this->assertEquals( 'Squiz.PHP.Heredoc.NotAllowed', $errors['load.php'][28][8][0]['code'] );
+
// Check for WordPress.WP.DeprecatedFunctions.the_author_emailFound error on Line no 12 and column no at 5.
$this->assertArrayHasKey( 12, $errors['load.php'] );
$this->assertArrayHasKey( 5, $errors['load.php'][12] );
$this->assertArrayHasKey( 'code', $errors['load.php'][12][5][0] );
$this->assertEquals( 'WordPress.WP.DeprecatedFunctions.the_author_emailFound', $errors['load.php'][12][5][0]['code'] );
+ // Check for PluginCheck.CodeAnalysis.RequiredFunctionParameters.parse_str_resultMissing error on Line no 34 and column no at 1.
+ $this->assertSame( 'PluginCheck.CodeAnalysis.RequiredFunctionParameters.parse_str_resultMissing', $errors['load.php'][34][1][0]['code'] );
+
// Check for WordPress.Security.ValidatedSanitizedInput warnings on Line no 15 and column no at 27.
$this->assertCount( 1, wp_list_filter( $warnings['load.php'][15][27], array( 'code' => 'WordPress.Security.ValidatedSanitizedInput.InputNotValidated' ) ) );
$this->assertCount( 1, wp_list_filter( $warnings['load.php'][15][27], array( 'code' => 'WordPress.Security.ValidatedSanitizedInput.MissingUnslash' ) ) );