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
143 changes: 143 additions & 0 deletions includes/Checker/Checks/General/Php_Error_Reporting_Check.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
<?php
/**
* Class Php_Error_Reporting_Check.
*
* @package plugin-check
*/

namespace WordPress\Plugin_Check\Checker\Checks\General;

use WordPress\Plugin_Check\Checker\Check_Categories;
use WordPress\Plugin_Check\Checker\Check_Result;
use WordPress\Plugin_Check\Checker\Checks\Abstract_PHP_CodeSniffer_Check;
use WordPress\Plugin_Check\Traits\Stable_Check;

/**
* Check for production-time PHP error reporting changes.
*
* Delegates detection to the PhpErrorReportingSniff and translates its
* per-pattern error codes into a single user-facing warning.
*
* @since 1.9.0
*/
class Php_Error_Reporting_Check extends Abstract_PHP_CodeSniffer_Check {

use Stable_Check;

/**
* Gets the categories for the check.
*
* Every check must have at least one category.
*
* @since 1.9.0
*
* @return array The categories for the check.
*/
public function get_categories() {
return array(
Check_Categories::CATEGORY_GENERAL,
);
}

/**
* Returns an associative array of arguments to pass to PHPCS.
*
* @since 1.9.0
*
* @param Check_Result $result The check result to amend, including the plugin context to check.
* @return array An associative array of PHPCS CLI arguments.
*/
protected function get_args( Check_Result $result ) {
return array(
'extensions' => 'php',
'standard' => 'PluginCheck',
'sniffs' => 'PluginCheck.CodeAnalysis.PhpErrorReporting',
);
}

/**
* Gets the description for the check.
*
* Every check must have a short description explaining what the check does.
*
* @since 1.9.0
*
* @return string Description.
*/
public function get_description(): string {
return __( 'Detects runtime changes to PHP error reporting configuration or WordPress debug constants.', 'plugin-check' );
}

/**
* Gets the documentation URL for the check.
*
* Every check must have a URL with further information about the check.
*
* @since 1.9.0
*
* @return string The documentation URL.
*/
public function get_documentation_url(): string {
return 'https://www.php.net/manual/en/function.error-reporting.php';
}

/**
* Amends the given result with a message for the specified file.
*
* Translates each PhpErrorReportingSniff error code into a single unified
* warning code (`php_error_reporting_detected`) so the check exposes one
* stable, user-facing message regardless of which pattern was detected.
*
* @since 1.9.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 ) {
if ( 0 === strpos( $code, 'PluginCheck.CodeAnalysis.PhpErrorReporting.' ) ) {
$warning_message = sprintf(
'<strong>%1$s</strong><br><br>%2$s<br><br>%3$s<br><br>%4$s',
__( 'Do not change PHP error reporting in production code', 'plugin-check' ),
__( 'A plugin should not modify PHP\'s error-reporting configuration. Calls such as <code>error_reporting()</code>, <code>ini_set(\'display_errors\', &hellip;)</code>, or redefining <code>WP_DEBUG</code>, <code>WP_DEBUG_LOG</code>, <code>WP_DEBUG_DISPLAY</code> or <code>SCRIPT_DEBUG</code> change behaviour for every other plugin and theme on the site.', 'plugin-check' ),
__( 'This can leak sensitive information (paths, secrets, stack traces) and breaks the standard debugging workflow for site owners and other developers. The host\'s <code>php.ini</code> and the site\'s <code>wp-config.php</code> are the correct places to control this.', 'plugin-check' ),
__( 'Please remove these calls, or move them behind a strictly developer-only flag that is never set in shipped code.', 'plugin-check' )
);

$docs = $this->get_documentation_url();
$code = 'php_error_reporting_detected';
$severity = 8;

parent::add_result_message_for_file(
$result,
false,
$warning_message,
$code,
$file,
$line,
$column,
$docs,
$severity
);
return;
}

parent::add_result_message_for_file(
$result,
$error,
$message,
$code,
$file,
$line,
$column,
$docs,
$severity
);
}
}
1 change: 1 addition & 0 deletions includes/Checker/Default_Check_Repository.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ private function register_default_checks() {
'wp_plugin_check_checks',
array(
'i18n_usage' => new Checks\General\I18n_Usage_Check(),
'php_error_reporting' => new Checks\General\Php_Error_Reporting_Check(),
'enqueued_scripts_size' => new Checks\Performance\Enqueued_Scripts_Size_Check(),
'enqueued_styles_size' => new Checks\Performance\Enqueued_Styles_Size_Check(),
'code_obfuscation' => new Checks\Plugin_Repo\Code_Obfuscation_Check(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
<?php
/**
* PhpErrorReportingSniff
*
* Detects runtime changes to PHP error reporting configuration and
* WordPress debug constants. A plugin must never call error_reporting(),
* ini_set()/ini_alter() for error_reporting/display_errors, or define
* WP_DEBUG, WP_DEBUG_LOG, WP_DEBUG_DISPLAY, SCRIPT_DEBUG in production.
*
* @package plugin-check
* @since 1.9.0
*/

namespace PluginCheckCS\PluginCheck\Sniffs\CodeAnalysis;

use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Util\Tokens;
use PHPCSUtils\Utils\MessageHelper;
use PHPCSUtils\Utils\PassedParameters;

/**
* Flags PHP error reporting changes and debug constant overrides.
*
* @since 1.9.0
*/
final class PhpErrorReportingSniff implements Sniff {

/**
* WordPress debug constants that must never be redefined by a plugin.
*
* @since 1.9.0
* @var string[]
*/
private const DEBUG_CONSTANTS = array(
'WP_DEBUG',
'WP_DEBUG_LOG',
'WP_DEBUG_DISPLAY',
'SCRIPT_DEBUG',
);

/**
* INI directives that control error reporting.
*
* @since 1.9.0
* @var string[]
*/
private const ERROR_INI_DIRECTIVES = array(
'error_reporting',
'display_errors',
);

/**
* Functions to inspect for the error reporting pattern.
*
* @since 1.9.0
* @var string[]
*/
private const TARGET_FUNCTIONS = array(
'error_reporting',
'ini_set',
'ini_alter',
'define',
);

/**
* Returns the array of tokens this sniff listens for.
*
* @since 1.9.0
*
* @return array<int>
*/
public function register() {
return array(
T_STRING,
T_CONST,
);
}

/**
* Processes a matched token.
*
* @since 1.9.0
*
* @param File $phpcsFile The file being scanned.
* @param int $stackPtr The position of the current token.
*
* @return void
*/
public function process( File $phpcsFile, $stackPtr ) {
$tokens = $phpcsFile->getTokens();

if ( T_CONST === $tokens[ $stackPtr ]['code'] ) {
$this->check_const_declaration( $phpcsFile, $stackPtr, $tokens );
return;
}

$content = strtolower( $tokens[ $stackPtr ]['content'] );
if ( ! in_array( $content, self::TARGET_FUNCTIONS, true ) ) {
return;
}

// Must be followed by an opening parenthesis (function call), not a class/namespace reference.
$next_non_empty = $phpcsFile->findNext( Tokens::$emptyTokens, $stackPtr + 1, null, true );
if ( false === $next_non_empty || T_OPEN_PARENTHESIS !== $tokens[ $next_non_empty ]['code'] ) {
return;
}

$this->check_function_call( $phpcsFile, $stackPtr, $tokens, $content );
}

/**
* Inspects a function call and reports a violation if the pattern matches.
*
* @since 1.9.0
*
* @param File $phpcsFile The file being scanned.
* @param int $stackPtr Position of the function name token.
* @param array $tokens Token stack.
* @param string $func_name Lowercase function name.
*
* @return void
*/
private function check_function_call( File $phpcsFile, $stackPtr, $tokens, $func_name ) {
$opener = $phpcsFile->findNext( T_OPEN_PARENTHESIS, $stackPtr + 1 );
if ( false === $opener || ! isset( $tokens[ $opener ]['parenthesis_closer'] ) ) {
return;
}

// Direct call to error_reporting() — no argument check needed.
if ( 'error_reporting' === $func_name ) {
$this->report( $phpcsFile, $stackPtr, 'DirectErrorReportingCall' );
return;
}

// Resolve the actual first parameter (token-based find is wrong for calls like
// `ini_set( some_function(), 'error_reporting' )`).
$params = PassedParameters::getParameters( $phpcsFile, $stackPtr );
if ( empty( $params ) || ! isset( $params[1] ) ) {
return;
}

$first_string = $phpcsFile->findNext(
T_CONSTANT_ENCAPSED_STRING,
$params[1]['start'],
$params[1]['end'] + 1
);
if ( false === $first_string ) {
return;
}

$argument = trim( $tokens[ $first_string ]['content'], "\"' \t" );

if ( 'ini_set' === $func_name || 'ini_alter' === $func_name ) {
$normalized = strtolower( $argument );
if ( in_array( $normalized, self::ERROR_INI_DIRECTIVES, true ) ) {
$this->report(
$phpcsFile,
$stackPtr,
MessageHelper::stringToErrorcode( 'IniDirective' . ucfirst( $normalized ) )
);
}
return;
}

if ( 'define' === $func_name && in_array( $argument, self::DEBUG_CONSTANTS, true ) ) {
$this->report(
$phpcsFile,
$stackPtr,
MessageHelper::stringToErrorcode( 'Define' . $argument )
);
}
}

/**
* Inspects a `const` declaration block for debug constants.
*
* Handles both single (`const WP_DEBUG = true;`) and comma-separated
* (`const A = 1, WP_DEBUG = true;`) declarations.
*
* @since 1.9.0
*
* @param File $phpcsFile The file being scanned.
* @param int $stackPtr Position of the T_CONST token.
* @param array $tokens Token stack.
*
* @return void
*/
private function check_const_declaration( File $phpcsFile, $stackPtr, $tokens ) {
$end = $phpcsFile->findNext( array( T_SEMICOLON, T_OPEN_CURLY_BRACKET ), $stackPtr + 1 );
if ( false === $end ) {
return;
}

for ( $i = $stackPtr + 1; $i < $end; $i++ ) {
if ( T_STRING === $tokens[ $i ]['code'] && in_array( $tokens[ $i ]['content'], self::DEBUG_CONSTANTS, true ) ) {
$this->report(
$phpcsFile,
$i,
MessageHelper::stringToErrorcode( 'Const' . $tokens[ $i ]['content'] )
);
}
}
}

/**
* Emits a single, stable error for any detected pattern.
*
* The check layer translates this to its own user-facing message and severity.
*
* @since 1.9.0
*
* @param File $phpcsFile The file being scanned.
* @param int $stackPtr Position of the matched token.
* @param string $code Error code suffix (sniff-specific).
*
* @return void
*/
private function report( File $phpcsFile, $stackPtr, $code ) {
$tokens = $phpcsFile->getTokens();
$is_const = ( T_CONST === $tokens[ $stackPtr ]['code'] );
$message = $is_const
? 'Detected production-time debug constant definition: %s.'
: 'Detected production-time change to PHP error reporting: %s().';

MessageHelper::addMessage(
$phpcsFile,
$message,
$stackPtr,
true,
$code,
array( $tokens[ $stackPtr ]['content'] )
);
}
}
Loading
Loading