Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DB/PreparedSQLPlaceholders: use PHPCSUtils, allow for modern PHP and some bug fixes #2314

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
fcea43c
DB/PreparedSQLPlaceholders: add extra tests
jrfnl Jan 11, 2023
671289d
DB/PreparedSQLPlaceholders: make variables used explicit
jrfnl Jul 22, 2023
27ca935
DB/PreparedSQLPlaceholders: implement PHPCSUtils
jrfnl Jan 11, 2023
0f7b3ad
DB/PreparedSQLPlaceholders: minor simplification
jrfnl Jul 22, 2023
7004173
DB/PreparedSQLPlaceholders: prevent false positive due to comments in…
jrfnl Jul 22, 2023
abcba3d
DB/PreparedSQLPlaceholders: prevent false negative due to comments in…
jrfnl Jul 22, 2023
4fc71c3
DB/PreparedSQLPlaceholders: prevent false positive due to comments in…
jrfnl Jul 22, 2023
eee85c0
DB/PreparedSQLPlaceholders: bug fix - allow for FQN function calls
jrfnl Jul 22, 2023
7a4259f
DB/PreparedSQLPlaceholders: bug fix - sniff could skip too much
jrfnl Jul 22, 2023
7d06fbb
DB/PreparedSQLPlaceholders: improve error message precision for Ident…
jrfnl Jul 22, 2023
bc75ffe
DB/PreparedSQLPlaceholders: improve error message precision for Quote…
jrfnl Jul 22, 2023
01c5bf4
DB/PreparedSQLPlaceholders: prevent token walking too far
jrfnl Jul 22, 2023
e2f237f
DB/PreparedSQLPlaceholders: add tests with PHP 7.3+ trailing commas i…
jrfnl Jul 22, 2023
d61a616
DB/PreparedSQLPlaceholders: add tests with PHP 8.0+ nullsafe object o…
jrfnl Jul 22, 2023
b1c9095
DB/PreparedSQLPlaceholders: add tests with PHP 8.0+ nullsafe object o…
jrfnl Jul 22, 2023
8f745ff
DB/PreparedSQLPlaceholders: add support for PHP 8.0+ named parameters…
jrfnl Jan 11, 2023
7e13937
DB/PreparedSQLPlaceholders: add support for PHP 8.0+ named parameters…
jrfnl Jan 11, 2023
995ff43
DB/PreparedSQLPlaceholders: add support for PHP 8.0+ named parameters…
jrfnl Apr 8, 2023
6063a6a
DB/PreparedSQLPlaceholders: add support for PHP 8.0+ named parameters…
jrfnl Jul 22, 2023
57df20c
DB/PreparedSQLPlaceholders: add tests with PHP 8.1+ first class callable
jrfnl Jul 22, 2023
9045552
DB/PreparedSQLPlaceholders: minor documentation fix
jrfnl Apr 12, 2023
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
148 changes: 94 additions & 54 deletions WordPress/Sniffs/DB/PreparedSQLPlaceholdersSniff.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,18 @@
namespace WordPressCS\WordPress\Sniffs\DB;

use PHP_CodeSniffer\Util\Tokens;
use PHPCSUtils\Tokens\Collections;
use PHPCSUtils\Utils\Arrays;
use PHPCSUtils\Utils\PassedParameters;
use PHPCSUtils\Utils\TextStrings;
use WordPressCS\WordPress\Helpers\MinimumWPVersionTrait;
use WordPressCS\WordPress\Helpers\WPDBTrait;
use WordPressCS\WordPress\Sniff;

/**
* Check for incorrect use of the $wpdb->prepare method.
* Checks for incorrect use of the $wpdb->prepare method.
*
* Check the following issues:
* Checks the following issues:
* - The only placeholders supported are: %d, %f (%F), %s, %i, and their variations.
* - Literal % signs need to be properly escaped as `%%`.
* - Simple placeholders (%d, %f, %F, %s, %i) should be left unquoted in the query string.
Expand Down Expand Up @@ -190,7 +192,11 @@ public function process_token( $stackPtr ) {
return;
}

$query = $parameters[1];
$query = PassedParameters::getParameterFromStack( $parameters, 1, 'query' );
if ( false === $query ) {
return;
}

$text_string_tokens_found = false;
$variable_found = false;
$sql_wildcard_found = false;
Expand All @@ -201,10 +207,12 @@ public function process_token( $stackPtr ) {
'implode_fill' => 0,
'adjustment_count' => 0,
);
$skip_from = null;
$skip_to = null;

for ( $i = $query['start']; $i <= $query['end']; $i++ ) {
// Skip over groups of tokens if they are part of an inline function call.
if ( isset( $skip_from, $skip_to ) && $i >= $skip_from && $i < $skip_to ) {
if ( isset( $skip_from, $skip_to ) && $i >= $skip_from && $i <= $skip_to ) {
$i = $skip_to;
continue;
}
Expand All @@ -224,50 +232,80 @@ public function process_token( $stackPtr ) {
$sprintf_parameters = PassedParameters::getParameters( $this->phpcsFile, $i );

if ( ! empty( $sprintf_parameters ) ) {
/*
* Check for named params. sprintf() does not support this due to its variadic nature,
* and we cannot analyse the code correctly if it is used, so skip the whole sprintf()
* in that case.
*/
$valid_sprintf = true;
foreach ( $sprintf_parameters as $param ) {
if ( isset( $param['name'] ) ) {
$valid_sprintf = false;
break;
}
}

if ( false === $valid_sprintf ) {
$next = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $i + 1 ), null, true );
if ( \T_OPEN_PARENTHESIS === $this->tokens[ $next ]['code']
&& isset( $this->tokens[ $next ]['parenthesis_closer'] )
) {
$skip_from = ( $i + 1 );
$skip_to = $this->tokens[ $next ]['parenthesis_closer'];
}

continue;
}

// We know for sure this sprintf() uses positional parameters, so this will be fine.
$skip_from = ( $sprintf_parameters[1]['end'] + 1 );
$last_param = end( $sprintf_parameters );
$skip_to = ( $last_param['end'] + 1 );

$valid_in_clauses['implode_fill'] += $this->analyse_sprintf( $sprintf_parameters );
$valid_in_clauses['adjustment_count'] += ( \count( $sprintf_parameters ) - 1 );
}
unset( $sprintf_parameters, $last_param );
unset( $sprintf_parameters, $valid_sprintf, $last_param );

} elseif ( 'implode' === strtolower( $this->tokens[ $i ]['content'] ) ) {
$prev = $this->phpcsFile->findPrevious(
Tokens::$textStringTokens,
Tokens::$emptyTokens + array( \T_STRING_CONCAT => \T_STRING_CONCAT ),
( $i - 1 ),
$query['start']
$query['start'],
true
);

$prev_content = TextStrings::stripQuotes( $this->tokens[ $prev ]['content'] );
$regex_quote = $this->get_regex_quote_snippet( $prev_content, $this->tokens[ $prev ]['content'] );
if ( isset( Tokens::$textStringTokens[ $this->tokens[ $prev ]['code'] ] ) ) {
$prev_content = TextStrings::stripQuotes( $this->tokens[ $prev ]['content'] );
$regex_quote = $this->get_regex_quote_snippet( $prev_content, $this->tokens[ $prev ]['content'] );

// Only examine the implode if preceded by an ` IN (`.
if ( preg_match( '`\s+IN\s*\(\s*(' . $regex_quote . ')?$`i', $prev_content, $match ) > 0 ) {

if ( isset( $match[1] ) && $regex_quote !== $this->regex_quote ) {
$this->phpcsFile->addError(
'Dynamic placeholder generation should not have surrounding quotes.',
$i,
'QuotedDynamicPlaceholderGeneration'
);
}
// Only examine the implode if preceded by an ` IN (`.
if ( preg_match( '`\s+IN\s*\(\s*(' . $regex_quote . ')?$`i', $prev_content, $match ) > 0 ) {

if ( $this->analyse_implode( $i ) === true ) {
++$valid_in_clauses['uses_in'];
++$valid_in_clauses['implode_fill'];
if ( isset( $match[1] ) && $regex_quote !== $this->regex_quote ) {
$this->phpcsFile->addError(
'Dynamic placeholder generation should not have surrounding quotes.',
$prev,
'QuotedDynamicPlaceholderGeneration'
);
}

$next = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $i + 1 ), null, true );
if ( \T_OPEN_PARENTHESIS === $this->tokens[ $next ]['code']
&& isset( $this->tokens[ $next ]['parenthesis_closer'] )
) {
$skip_from = ( $i + 1 );
$skip_to = ( $this->tokens[ $next ]['parenthesis_closer'] + 1 );
if ( $this->analyse_implode( $i ) === true ) {
++$valid_in_clauses['uses_in'];
++$valid_in_clauses['implode_fill'];

$next = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $i + 1 ), null, true );
if ( \T_OPEN_PARENTHESIS === $this->tokens[ $next ]['code']
&& isset( $this->tokens[ $next ]['parenthesis_closer'] )
) {
$skip_from = ( $i + 1 );
$skip_to = $this->tokens[ $next ]['parenthesis_closer'];
}
}
}
unset( $next, $prev_content, $regex_quote, $match );
}
unset( $prev, $next, $prev_content, $regex_quote, $match );
unset( $prev );
}
}

Expand Down Expand Up @@ -537,20 +575,22 @@ function ( $symbol ) {
}

$replacements = $parameters;
array_shift( $replacements ); // Remove the query.
unset( $replacements['query'], $replacements[1] ); // Remove the query param, whether passed positionally or named.

// The parameters may have been passed as an array in parameter 2.
if ( isset( $parameters[2] ) && 2 === $total_parameters ) {
// The parameters may have been passed as an array in the variadic $args parameter.
$args_param = PassedParameters::getParameterFromStack( $parameters, 2, 'args' );
if ( false !== $args_param && 2 === $total_parameters ) {
$next = $this->phpcsFile->findNext(
Tokens::$emptyTokens,
$parameters[2]['start'],
( $parameters[2]['end'] + 1 ),
$args_param['start'],
( $args_param['end'] + 1 ),
true
);

if ( false !== $next
&& ( \T_ARRAY === $this->tokens[ $next ]['code']
|| \T_OPEN_SHORT_ARRAY === $this->tokens[ $next ]['code'] )
|| ( isset( Collections::shortArrayListOpenTokensBC()[ $this->tokens[ $next ]['code'] ] )
&& Arrays::isShortArray( $this->phpcsFile, $next ) === true ) )
) {
$replacements = PassedParameters::getParameters( $this->phpcsFile, $next );
}
Expand Down Expand Up @@ -627,15 +667,11 @@ protected function get_regex_quote_snippet( $stripped_content, $original_content
protected function analyse_sprintf( $sprintf_params ) {
$found = 0;

unset( $sprintf_params[1] );
unset( $sprintf_params[1] ); // Remove the positionally passed $format param.

foreach ( $sprintf_params as $sprintf_param ) {
if ( strpos( strtolower( $sprintf_param['raw'] ), 'implode' ) === false ) {
continue;
}

$implode = $this->phpcsFile->findNext(
Tokens::$emptyTokens,
Tokens::$emptyTokens + array( \T_NS_SEPARATOR => \T_NS_SEPARATOR ),
$sprintf_param['start'],
$sprintf_param['end'],
true
Expand Down Expand Up @@ -675,23 +711,26 @@ protected function analyse_sprintf( $sprintf_params ) {
*/
protected function analyse_implode( $implode_token ) {
$implode_params = PassedParameters::getParameters( $this->phpcsFile, $implode_token );

if ( empty( $implode_params ) || \count( $implode_params ) !== 2 ) {
return false;
}

if ( preg_match( '`^(["\']), ?\1$`', $implode_params[1]['raw'] ) !== 1 ) {
$implode_separator_param = PassedParameters::getParameterFromStack( $implode_params, 1, 'separator' );
if ( false === $implode_separator_param
|| preg_match( '`^(["\']), ?\1$`', $implode_separator_param['clean'] ) !== 1
) {
return false;
}

if ( strpos( strtolower( $implode_params[2]['raw'] ), 'array_fill' ) === false ) {
$implode_array_param = PassedParameters::getParameterFromStack( $implode_params, 2, 'array' );
if ( false === $implode_array_param ) {
return false;
}

$array_fill = $this->phpcsFile->findNext(
Tokens::$emptyTokens,
$implode_params[2]['start'],
$implode_params[2]['end'],
Tokens::$emptyTokens + array( \T_NS_SEPARATOR => \T_NS_SEPARATOR ),
$implode_array_param['start'],
$implode_array_param['end'],
true
);

Expand All @@ -701,23 +740,24 @@ protected function analyse_implode( $implode_token ) {
return false;
}

$array_fill_params = PassedParameters::getParameters( $this->phpcsFile, $array_fill );

if ( empty( $array_fill_params ) || \count( $array_fill_params ) !== 3 ) {
$array_fill_value_param = PassedParameters::getParameter( $this->phpcsFile, $array_fill, 3, 'value' );
if ( false === $array_fill_value_param ) {
return false;
}

if ( "'%i'" === $array_fill_params[3]['raw']
|| '"%i"' === $array_fill_params[3]['raw']
if ( "'%i'" === $array_fill_value_param['clean']
|| '"%i"' === $array_fill_value_param['clean']
) {
$firstNonEmpty = $this->phpcsFile->findNext( Tokens::$emptyTokens, $array_fill_value_param['start'], $array_fill_value_param['end'], true );

$this->phpcsFile->addError(
'The %i placeholder cannot be used within SQL `IN()` clauses.',
$implode_token,
$firstNonEmpty,
'IdentifierWithinIN'
);
return false;
}

return (bool) preg_match( '`^(["\'])%[dfFs]\1$`', $array_fill_params[3]['raw'] );
return (bool) preg_match( '`^(["\'])%[dfFs]\1$`', $array_fill_value_param['clean'] );
}
}
Loading