10
10
namespace WordPressCS \WordPress \Sniffs \DB ;
11
11
12
12
use PHP_CodeSniffer \Util \Tokens ;
13
+ use PHPCSUtils \Tokens \Collections ;
14
+ use PHPCSUtils \Utils \Arrays ;
13
15
use PHPCSUtils \Utils \PassedParameters ;
14
16
use PHPCSUtils \Utils \TextStrings ;
15
17
use WordPressCS \WordPress \Helpers \MinimumWPVersionTrait ;
16
18
use WordPressCS \WordPress \Helpers \WPDBTrait ;
17
19
use WordPressCS \WordPress \Sniff ;
18
20
19
21
/**
20
- * Check for incorrect use of the $wpdb->prepare method.
22
+ * Checks for incorrect use of the $wpdb->prepare method.
21
23
*
22
- * Check the following issues:
24
+ * Checks the following issues:
23
25
* - The only placeholders supported are: %d, %f (%F), %s, %i, and their variations.
24
26
* - Literal % signs need to be properly escaped as `%%`.
25
27
* - Simple placeholders (%d, %f, %F, %s, %i) should be left unquoted in the query string.
@@ -190,7 +192,11 @@ public function process_token( $stackPtr ) {
190
192
return ;
191
193
}
192
194
193
- $ query = $ parameters [1 ];
195
+ $ query = PassedParameters::getParameterFromStack ( $ parameters , 1 , 'query ' );
196
+ if ( false === $ query ) {
197
+ return ;
198
+ }
199
+
194
200
$ text_string_tokens_found = false ;
195
201
$ variable_found = false ;
196
202
$ sql_wildcard_found = false ;
@@ -201,10 +207,12 @@ public function process_token( $stackPtr ) {
201
207
'implode_fill ' => 0 ,
202
208
'adjustment_count ' => 0 ,
203
209
);
210
+ $ skip_from = null ;
211
+ $ skip_to = null ;
204
212
205
213
for ( $ i = $ query ['start ' ]; $ i <= $ query ['end ' ]; $ i ++ ) {
206
214
// Skip over groups of tokens if they are part of an inline function call.
207
- if ( isset ( $ skip_from , $ skip_to ) && $ i >= $ skip_from && $ i < $ skip_to ) {
215
+ if ( isset ( $ skip_from , $ skip_to ) && $ i >= $ skip_from && $ i <= $ skip_to ) {
208
216
$ i = $ skip_to ;
209
217
continue ;
210
218
}
@@ -224,50 +232,80 @@ public function process_token( $stackPtr ) {
224
232
$ sprintf_parameters = PassedParameters::getParameters ( $ this ->phpcsFile , $ i );
225
233
226
234
if ( ! empty ( $ sprintf_parameters ) ) {
235
+ /*
236
+ * Check for named params. sprintf() does not support this due to its variadic nature,
237
+ * and we cannot analyse the code correctly if it is used, so skip the whole sprintf()
238
+ * in that case.
239
+ */
240
+ $ valid_sprintf = true ;
241
+ foreach ( $ sprintf_parameters as $ param ) {
242
+ if ( isset ( $ param ['name ' ] ) ) {
243
+ $ valid_sprintf = false ;
244
+ break ;
245
+ }
246
+ }
247
+
248
+ if ( false === $ valid_sprintf ) {
249
+ $ next = $ this ->phpcsFile ->findNext ( Tokens::$ emptyTokens , ( $ i + 1 ), null , true );
250
+ if ( \T_OPEN_PARENTHESIS === $ this ->tokens [ $ next ]['code ' ]
251
+ && isset ( $ this ->tokens [ $ next ]['parenthesis_closer ' ] )
252
+ ) {
253
+ $ skip_from = ( $ i + 1 );
254
+ $ skip_to = $ this ->tokens [ $ next ]['parenthesis_closer ' ];
255
+ }
256
+
257
+ continue ;
258
+ }
259
+
260
+ // We know for sure this sprintf() uses positional parameters, so this will be fine.
227
261
$ skip_from = ( $ sprintf_parameters [1 ]['end ' ] + 1 );
228
262
$ last_param = end ( $ sprintf_parameters );
229
263
$ skip_to = ( $ last_param ['end ' ] + 1 );
230
264
231
265
$ valid_in_clauses ['implode_fill ' ] += $ this ->analyse_sprintf ( $ sprintf_parameters );
232
266
$ valid_in_clauses ['adjustment_count ' ] += ( \count ( $ sprintf_parameters ) - 1 );
233
267
}
234
- unset( $ sprintf_parameters , $ last_param );
268
+ unset( $ sprintf_parameters , $ valid_sprintf , $ last_param );
235
269
236
270
} elseif ( 'implode ' === strtolower ( $ this ->tokens [ $ i ]['content ' ] ) ) {
237
271
$ prev = $ this ->phpcsFile ->findPrevious (
238
- Tokens::$ textStringTokens ,
272
+ Tokens::$ emptyTokens + array ( \ T_STRING_CONCAT => \ T_STRING_CONCAT ) ,
239
273
( $ i - 1 ),
240
- $ query ['start ' ]
274
+ $ query ['start ' ],
275
+ true
241
276
);
242
277
243
- $ prev_content = TextStrings::stripQuotes ( $ this ->tokens [ $ prev ]['content ' ] );
244
- $ regex_quote = $ this ->get_regex_quote_snippet ( $ prev_content , $ this ->tokens [ $ prev ]['content ' ] );
278
+ if ( isset ( Tokens::$ textStringTokens [ $ this ->tokens [ $ prev ]['code ' ] ] ) ) {
279
+ $ prev_content = TextStrings::stripQuotes ( $ this ->tokens [ $ prev ]['content ' ] );
280
+ $ regex_quote = $ this ->get_regex_quote_snippet ( $ prev_content , $ this ->tokens [ $ prev ]['content ' ] );
245
281
246
- // Only examine the implode if preceded by an ` IN (`.
247
- if ( preg_match ( '`\s+IN\s*\(\s*( ' . $ regex_quote . ')?$`i ' , $ prev_content , $ match ) > 0 ) {
248
-
249
- if ( isset ( $ match [1 ] ) && $ regex_quote !== $ this ->regex_quote ) {
250
- $ this ->phpcsFile ->addError (
251
- 'Dynamic placeholder generation should not have surrounding quotes. ' ,
252
- $ i ,
253
- 'QuotedDynamicPlaceholderGeneration '
254
- );
255
- }
282
+ // Only examine the implode if preceded by an ` IN (`.
283
+ if ( preg_match ( '`\s+IN\s*\(\s*( ' . $ regex_quote . ')?$`i ' , $ prev_content , $ match ) > 0 ) {
256
284
257
- if ( $ this ->analyse_implode ( $ i ) === true ) {
258
- ++$ valid_in_clauses ['uses_in ' ];
259
- ++$ valid_in_clauses ['implode_fill ' ];
285
+ if ( isset ( $ match [1 ] ) && $ regex_quote !== $ this ->regex_quote ) {
286
+ $ this ->phpcsFile ->addError (
287
+ 'Dynamic placeholder generation should not have surrounding quotes. ' ,
288
+ $ prev ,
289
+ 'QuotedDynamicPlaceholderGeneration '
290
+ );
291
+ }
260
292
261
- $ next = $ this ->phpcsFile ->findNext ( Tokens::$ emptyTokens , ( $ i + 1 ), null , true );
262
- if ( \T_OPEN_PARENTHESIS === $ this ->tokens [ $ next ]['code ' ]
263
- && isset ( $ this ->tokens [ $ next ]['parenthesis_closer ' ] )
264
- ) {
265
- $ skip_from = ( $ i + 1 );
266
- $ skip_to = ( $ this ->tokens [ $ next ]['parenthesis_closer ' ] + 1 );
293
+ if ( $ this ->analyse_implode ( $ i ) === true ) {
294
+ ++$ valid_in_clauses ['uses_in ' ];
295
+ ++$ valid_in_clauses ['implode_fill ' ];
296
+
297
+ $ next = $ this ->phpcsFile ->findNext ( Tokens::$ emptyTokens , ( $ i + 1 ), null , true );
298
+ if ( \T_OPEN_PARENTHESIS === $ this ->tokens [ $ next ]['code ' ]
299
+ && isset ( $ this ->tokens [ $ next ]['parenthesis_closer ' ] )
300
+ ) {
301
+ $ skip_from = ( $ i + 1 );
302
+ $ skip_to = $ this ->tokens [ $ next ]['parenthesis_closer ' ];
303
+ }
267
304
}
268
305
}
306
+ unset( $ next , $ prev_content , $ regex_quote , $ match );
269
307
}
270
- unset( $ prev, $ next , $ prev_content , $ regex_quote , $ match );
308
+ unset( $ prev );
271
309
}
272
310
}
273
311
@@ -537,20 +575,22 @@ function ( $symbol ) {
537
575
}
538
576
539
577
$ replacements = $ parameters ;
540
- array_shift ( $ replacements ); // Remove the query.
578
+ unset ( $ replacements[ ' query ' ], $ replacements [ 1 ] ); // Remove the query param, whether passed positionally or named .
541
579
542
- // The parameters may have been passed as an array in parameter 2.
543
- if ( isset ( $ parameters [2 ] ) && 2 === $ total_parameters ) {
580
+ // The parameters may have been passed as an array in the variadic $args parameter.
581
+ $ args_param = PassedParameters::getParameterFromStack ( $ parameters , 2 , 'args ' );
582
+ if ( false !== $ args_param && 2 === $ total_parameters ) {
544
583
$ next = $ this ->phpcsFile ->findNext (
545
584
Tokens::$ emptyTokens ,
546
- $ parameters [ 2 ] ['start ' ],
547
- ( $ parameters [ 2 ] ['end ' ] + 1 ),
585
+ $ args_param ['start ' ],
586
+ ( $ args_param ['end ' ] + 1 ),
548
587
true
549
588
);
550
589
551
590
if ( false !== $ next
552
591
&& ( \T_ARRAY === $ this ->tokens [ $ next ]['code ' ]
553
- || \T_OPEN_SHORT_ARRAY === $ this ->tokens [ $ next ]['code ' ] )
592
+ || ( isset ( Collections::shortArrayListOpenTokensBC ()[ $ this ->tokens [ $ next ]['code ' ] ] )
593
+ && Arrays::isShortArray ( $ this ->phpcsFile , $ next ) === true ) )
554
594
) {
555
595
$ replacements = PassedParameters::getParameters ( $ this ->phpcsFile , $ next );
556
596
}
@@ -627,15 +667,11 @@ protected function get_regex_quote_snippet( $stripped_content, $original_content
627
667
protected function analyse_sprintf ( $ sprintf_params ) {
628
668
$ found = 0 ;
629
669
630
- unset( $ sprintf_params [1 ] );
670
+ unset( $ sprintf_params [1 ] ); // Remove the positionally passed $format param.
631
671
632
672
foreach ( $ sprintf_params as $ sprintf_param ) {
633
- if ( strpos ( strtolower ( $ sprintf_param ['raw ' ] ), 'implode ' ) === false ) {
634
- continue ;
635
- }
636
-
637
673
$ implode = $ this ->phpcsFile ->findNext (
638
- Tokens::$ emptyTokens ,
674
+ Tokens::$ emptyTokens + array ( \ T_NS_SEPARATOR => \ T_NS_SEPARATOR ) ,
639
675
$ sprintf_param ['start ' ],
640
676
$ sprintf_param ['end ' ],
641
677
true
@@ -675,23 +711,26 @@ protected function analyse_sprintf( $sprintf_params ) {
675
711
*/
676
712
protected function analyse_implode ( $ implode_token ) {
677
713
$ implode_params = PassedParameters::getParameters ( $ this ->phpcsFile , $ implode_token );
678
-
679
714
if ( empty ( $ implode_params ) || \count ( $ implode_params ) !== 2 ) {
680
715
return false ;
681
716
}
682
717
683
- if ( preg_match ( '`^([" \']), ?\1$` ' , $ implode_params [1 ]['raw ' ] ) !== 1 ) {
718
+ $ implode_separator_param = PassedParameters::getParameterFromStack ( $ implode_params , 1 , 'separator ' );
719
+ if ( false === $ implode_separator_param
720
+ || preg_match ( '`^([" \']), ?\1$` ' , $ implode_separator_param ['clean ' ] ) !== 1
721
+ ) {
684
722
return false ;
685
723
}
686
724
687
- if ( strpos ( strtolower ( $ implode_params [2 ]['raw ' ] ), 'array_fill ' ) === false ) {
725
+ $ implode_array_param = PassedParameters::getParameterFromStack ( $ implode_params , 2 , 'array ' );
726
+ if ( false === $ implode_array_param ) {
688
727
return false ;
689
728
}
690
729
691
730
$ array_fill = $ this ->phpcsFile ->findNext (
692
- Tokens::$ emptyTokens ,
693
- $ implode_params [ 2 ] ['start ' ],
694
- $ implode_params [ 2 ] ['end ' ],
731
+ Tokens::$ emptyTokens + array ( \ T_NS_SEPARATOR => \ T_NS_SEPARATOR ) ,
732
+ $ implode_array_param ['start ' ],
733
+ $ implode_array_param ['end ' ],
695
734
true
696
735
);
697
736
@@ -701,23 +740,24 @@ protected function analyse_implode( $implode_token ) {
701
740
return false ;
702
741
}
703
742
704
- $ array_fill_params = PassedParameters::getParameters ( $ this ->phpcsFile , $ array_fill );
705
-
706
- if ( empty ( $ array_fill_params ) || \count ( $ array_fill_params ) !== 3 ) {
743
+ $ array_fill_value_param = PassedParameters::getParameter ( $ this ->phpcsFile , $ array_fill , 3 , 'value ' );
744
+ if ( false === $ array_fill_value_param ) {
707
745
return false ;
708
746
}
709
747
710
- if ( "'%i' " === $ array_fill_params [ 3 ][ ' raw ' ]
711
- || '"%i" ' === $ array_fill_params [ 3 ][ ' raw ' ]
748
+ if ( "'%i' " === $ array_fill_value_param [ ' clean ' ]
749
+ || '"%i" ' === $ array_fill_value_param [ ' clean ' ]
712
750
) {
751
+ $ firstNonEmpty = $ this ->phpcsFile ->findNext ( Tokens::$ emptyTokens , $ array_fill_value_param ['start ' ], $ array_fill_value_param ['end ' ], true );
752
+
713
753
$ this ->phpcsFile ->addError (
714
754
'The %i placeholder cannot be used within SQL `IN()` clauses. ' ,
715
- $ implode_token ,
755
+ $ firstNonEmpty ,
716
756
'IdentifierWithinIN '
717
757
);
718
758
return false ;
719
759
}
720
760
721
- return (bool ) preg_match ( '`^([" \'])%[dfFs]\1$` ' , $ array_fill_params [ 3 ][ ' raw ' ] );
761
+ return (bool ) preg_match ( '`^([" \'])%[dfFs]\1$` ' , $ array_fill_value_param [ ' clean ' ] );
722
762
}
723
763
}
0 commit comments