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' ) ) );