Skip to content

Commit 8fa98f6

Browse files
authored
Merge pull request #1668 from WordPress-Coding-Standards/feature/new-utility-method-is-in-function-call
New utility method Sniff::is_in_function_call() + implement use of it
2 parents cb8cd90 + 4e143ec commit 8fa98f6

File tree

6 files changed

+128
-22
lines changed

6 files changed

+128
-22
lines changed

WordPress/Sniff.php

Lines changed: 99 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1462,16 +1462,9 @@ protected function is_in_isset_or_empty( $stackPtr ) {
14621462
return true;
14631463
}
14641464

1465-
if ( \T_STRING === $previous_code && 'array_key_exists' === $this->tokens[ $previous_non_empty ]['content'] ) {
1466-
if ( $this->is_class_object_call( $previous_non_empty ) === true ) {
1467-
return false;
1468-
}
1469-
1470-
if ( $this->is_token_namespaced( $previous_non_empty ) === true ) {
1471-
return false;
1472-
}
1473-
1474-
$second_param = $this->get_function_call_parameter( $previous_non_empty, 2 );
1465+
$functionPtr = $this->is_in_function_call( $stackPtr, array( 'array_key_exists' => true ) );
1466+
if ( false !== $functionPtr ) {
1467+
$second_param = $this->get_function_call_parameter( $functionPtr, 2 );
14751468
if ( $stackPtr >= $second_param['start'] && $stackPtr <= $second_param['end'] ) {
14761469
return true;
14771470
}
@@ -1545,6 +1538,85 @@ protected function is_token_namespaced( $stackPtr ) {
15451538
return true;
15461539
}
15471540

1541+
/**
1542+
* Check if a token is (part of) a parameter for a function call to a select list of functions.
1543+
*
1544+
* This is useful, for instance, when trying to determine the context a variable is used in.
1545+
*
1546+
* For example: this function could be used to determine if the variable `$foo` is used
1547+
* in a global function call to the function `is_foo()`.
1548+
* In that case, a call to this function would return the stackPtr to the T_STRING `is_foo`
1549+
* for code like: `is_foo( $foo, 'some_other_param' )`, while it would return `false` for
1550+
* the following code `is_bar( $foo, 'some_other_param' )`.
1551+
*
1552+
* @since 2.1.0
1553+
*
1554+
* @param int $stackPtr The index of the token in the stack.
1555+
* @param array $valid_functions List of valid function names.
1556+
* Note: The keys to this array should be the function names
1557+
* in lowercase. Values are irrelevant.
1558+
* @param bool $global Optional. Whether to make sure that the function call is
1559+
* to a global function. If `false`, calls to methods, be it static
1560+
* `Class::method()` or via an object `$obj->method()`, and
1561+
* namespaced function calls, like `MyNS\function_name()` will
1562+
* also be accepted.
1563+
* Defaults to `true`.
1564+
* @param bool $allow_nested Optional. Whether to allow for nested function calls within the
1565+
* call to this function.
1566+
* I.e. when checking whether a token is within a function call
1567+
* to `strtolower()`, whether to accept `strtolower( trim( $var ) )`
1568+
* or only `strtolower( $var )`.
1569+
* Defaults to `false`.
1570+
*
1571+
* @return int|bool Stack pointer to the function call T_STRING token or false otherwise.
1572+
*/
1573+
protected function is_in_function_call( $stackPtr, $valid_functions, $global = true, $allow_nested = false ) {
1574+
if ( ! isset( $this->tokens[ $stackPtr ]['nested_parenthesis'] ) ) {
1575+
return false;
1576+
}
1577+
1578+
$nested_parenthesis = $this->tokens[ $stackPtr ]['nested_parenthesis'];
1579+
if ( false === $allow_nested ) {
1580+
$nested_parenthesis = array_reverse( $nested_parenthesis, true );
1581+
}
1582+
1583+
foreach ( $nested_parenthesis as $open => $close ) {
1584+
1585+
$prev_non_empty = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $open - 1 ), null, true, null, true );
1586+
if ( false === $prev_non_empty || \T_STRING !== $this->tokens[ $prev_non_empty ]['code'] ) {
1587+
continue;
1588+
}
1589+
1590+
if ( isset( $valid_functions[ strtolower( $this->tokens[ $prev_non_empty ]['content'] ) ] ) === false ) {
1591+
if ( false === $allow_nested ) {
1592+
// Function call encountered, but not to one of the allowed functions.
1593+
return false;
1594+
}
1595+
1596+
continue;
1597+
}
1598+
1599+
if ( false === $global ) {
1600+
return $prev_non_empty;
1601+
}
1602+
1603+
/*
1604+
* Now, make sure it is a global function.
1605+
*/
1606+
if ( $this->is_class_object_call( $prev_non_empty ) === true ) {
1607+
continue;
1608+
}
1609+
1610+
if ( $this->is_token_namespaced( $prev_non_empty ) === true ) {
1611+
continue;
1612+
}
1613+
1614+
return $prev_non_empty;
1615+
}
1616+
1617+
return false;
1618+
}
1619+
15481620
/**
15491621
* Check if something is only being sanitized.
15501622
*
@@ -1643,9 +1715,16 @@ protected function is_sanitized( $stackPtr, $require_unslash = false ) {
16431715
return true;
16441716
}
16451717

1646-
// If this isn't a call to a function, it sure isn't a sanitizing function.
1647-
if ( \T_STRING !== $this->tokens[ $functionPtr ]['code'] ) {
1648-
if ( $require_unslash ) {
1718+
$valid_functions = $this->sanitizingFunctions;
1719+
$valid_functions += $this->unslashingSanitizingFunctions;
1720+
$valid_functions['wp_unslash'] = true;
1721+
$valid_functions['array_map'] = true;
1722+
1723+
$functionPtr = $this->is_in_function_call( $stackPtr, $valid_functions );
1724+
1725+
// If this isn't a call to one of the valid functions, it sure isn't a sanitizing function.
1726+
if ( false === $functionPtr ) {
1727+
if ( true === $require_unslash ) {
16491728
$this->add_unslash_error( $stackPtr );
16501729
}
16511730

@@ -1657,15 +1736,17 @@ protected function is_sanitized( $stackPtr, $require_unslash = false ) {
16571736
// Check if wp_unslash() is being used.
16581737
if ( 'wp_unslash' === $functionName ) {
16591738

1660-
$is_unslashed = true;
1661-
$function_opener = array_pop( $nested_openers );
1739+
$is_unslashed = true;
1740+
1741+
unset( $valid_functions['wp_unslash'] );
1742+
$higherFunctionPtr = $this->is_in_function_call( $functionPtr, $valid_functions );
16621743

1663-
// If there is no other function being used, this value is unsanitized.
1664-
if ( ! isset( $function_opener ) ) {
1744+
// If there is no other valid function being used, this value is unsanitized.
1745+
if ( false === $higherFunctionPtr ) {
16651746
return false;
16661747
}
16671748

1668-
$functionPtr = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $function_opener - 1 ), null, true, null, true );
1749+
$functionPtr = $higherFunctionPtr;
16691750
$functionName = $this->tokens[ $functionPtr ]['content'];
16701751

16711752
} else {

WordPress/Sniffs/WP/CronIntervalSniff.php

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,15 @@ class CronIntervalSniff extends Sniff {
5555
'YEAR_IN_SECONDS' => 31536000,
5656
);
5757

58+
/**
59+
* Function within which the hook should be found.
60+
*
61+
* @var array
62+
*/
63+
protected $valid_functions = array(
64+
'add_filter' => true,
65+
);
66+
5867
/**
5968
* Returns an array of tokens this test wants to listen for.
6069
*
@@ -82,8 +91,8 @@ public function process_token( $stackPtr ) {
8291
}
8392

8493
// If within add_filter.
85-
$functionPtr = $this->phpcsFile->findPrevious( \T_STRING, key( $token['nested_parenthesis'] ) );
86-
if ( false === $functionPtr || 'add_filter' !== $this->tokens[ $functionPtr ]['content'] ) {
94+
$functionPtr = $this->is_in_function_call( $stackPtr, $this->valid_functions );
95+
if ( false === $functionPtr ) {
8796
return;
8897
}
8998

@@ -92,6 +101,11 @@ public function process_token( $stackPtr ) {
92101
return;
93102
}
94103

104+
if ( $stackPtr >= $callback['start'] ) {
105+
// "cron_schedules" found in the second parameter, not the first.
106+
return;
107+
}
108+
95109
// Detect callback function name.
96110
$callbackArrayPtr = $this->phpcsFile->findNext( Tokens::$emptyTokens, $callback['start'], ( $callback['end'] + 1 ), true );
97111

WordPress/Tests/Security/ValidatedSanitizedInputUnitTest.inc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,3 +211,8 @@ if ( ClassName::array_key_exists( 'my_field5', $_POST ) ) {
211211
}
212212

213213
echo sanitize_text_field (wp_unslash ($_GET['test'])); // OK.
214+
215+
if ( isset( $_GET['unslash_check'] ) ) {
216+
$clean = sanitize_text_field( WP_Faker::wp_unslash( $_GET['unslash_check'] ) ); // Bad x1 - unslash.
217+
$clean = WP_Faker\sanitize_text_field( wp_unslash( $_GET['unslash_check'] ) ); // Bad x1 - sanitize.
218+
}

WordPress/Tests/Security/ValidatedSanitizedInputUnitTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ public function getErrorList() {
5858
202 => 1,
5959
206 => 1,
6060
210 => 1,
61+
216 => 1,
62+
217 => 1,
6163
);
6264
}
6365

WordPress/Tests/WP/CronIntervalUnitTest.inc

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ add_filter( 'cron_schedules' ); // Ignore, no callback parameter.
5252

5353
add_filter( 'cron_schedules', [ 'Foo', 'my_add_quicklier' ] ); // Error: 5 min.
5454

55-
// Ignore, not our function. Currently gives false positive, will be fixed later when function call detection utility functions are added.
55+
// Ignore, not our function.
5656
My_Custom::add_filter( 'cron_schedules', [ 'Foo', 'my_add_quicklier' ] );
5757

5858
// Deal correctly with the WP time constants.
@@ -139,3 +139,8 @@ add_filter( 'cron_schedules', function ( $schedules ) {
139139
);
140140
return $schedules;
141141
} ); // Error: 9 min.
142+
143+
Custom::add_filter( 'cron_schedules', array( $class, $method ) ); // OK, not the WP function.
144+
add_filter( 'some_hook', array( $place, 'cron_schedules' ) ); // OK, not the hook we're looking for.
145+
add_filter( function() { return get_hook_name('cron_schedules'); }(), array( $class, $method ) ); // OK, nested in another function call.
146+

WordPress/Tests/WP/CronIntervalUnitTest.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ public function getWarningList() {
4545
41 => 1,
4646
43 => 1,
4747
53 => 1,
48-
56 => 1, // False positive.
4948
67 => 1,
5049
76 => 1,
5150
85 => 1,

0 commit comments

Comments
 (0)