wishthis/vendor/wp-coding-standards/wpcs/WordPress/Helpers/ValidationHelper.php
2023-09-20 13:52:46 +02:00

349 lines
11 KiB
PHP

<?php
/**
* WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
* @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
namespace WordPressCS\WordPress\Helpers;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Util\Tokens;
use PHPCSUtils\Tokens\Collections;
use PHPCSUtils\Utils\Conditions;
use PHPCSUtils\Utils\Context;
use PHPCSUtils\Utils\PassedParameters;
use PHPCSUtils\Utils\TextStrings;
use WordPressCS\WordPress\Helpers\ContextHelper;
use WordPressCS\WordPress\Helpers\VariableHelper;
/**
* Helper function for checking whether a token is validated.
*
* @since 3.0.0 The method in this class was previously contained in the
* `WordPressCS\WordPress\Sniff` class and has been moved here.
*/
final class ValidationHelper {
/**
* The tokens the function looks for to determine whether a token is validated.
*
* @since 3.0.0
*
* @var array<int|string, string>
*/
private static $targets = array(
\T_ISSET => 'construct',
\T_EMPTY => 'construct',
\T_STRING => 'function_call',
\T_COALESCE => 'coalesce',
\T_COALESCE_EQUAL => 'coalesce',
);
/**
* List of PHP native functions to check if an array index exists.
*
* @since 3.0.0
*
* @var array<string, bool>
*/
private static $key_exists_functions = array(
'array_key_exists' => true,
'key_exists' => true, // Alias.
);
/**
* Check if the existence of a variable is validated with isset(), empty(), array_key_exists()
* or key_exists().
*
* When $in_condition_only is `false`, (which is the default), this is considered
* valid:
*
* ```php
* if ( isset( $var ) ) {
* // Do stuff, like maybe return or exit (but could be anything)
* }
*
* foo( $var );
* ```
*
* When it is `true`, that would be invalid; the use of the variable must be within
* the scope of the validating condition, like this:
*
* ```php
* if ( isset( $var ) ) {
* foo( $var );
* }
* ```
*
* @since 0.5.0
* @since 2.1.0 Now recognizes array_key_exists() and key_exists() as validation functions.
* @since 2.1.0 Stricter check on whether the correct variable and the correct
* array keys are being validated.
* @since 3.0.0 - Moved from the Sniff class to this class.
* - The method visibility was changed from `protected` to `public static`.
* - The `$phpcsFile` parameter was added.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
* @param int $stackPtr The index of this token in the stack.
* @param array|string $array_keys An array key to check for ("bar" in $foo['bar'])
* or an array of keys for multi-level array access.
* @param bool $in_condition_only Whether to require that this use of the
* variable occurs within the scope of the
* validating condition, or just in the same
* scope (default).
*
* @return bool Whether the var is validated.
*/
public static function is_validated( File $phpcsFile, $stackPtr, $array_keys = array(), $in_condition_only = false ) {
$tokens = $phpcsFile->getTokens();
if ( isset( $tokens[ $stackPtr ] ) === false ) {
return false;
}
if ( $in_condition_only ) {
/*
* This is a stricter check, requiring the variable to be used only
* within the validation condition.
*/
$conditionPtr = Conditions::getLastCondition( $phpcsFile, $stackPtr );
if ( false === $conditionPtr ) {
// If there are no conditions, there's no validation.
return false;
}
$condition = $tokens[ $conditionPtr ];
if ( ! isset( $condition['parenthesis_opener'] ) ) {
// Live coding or parse error.
return false;
}
$scope_start = $condition['parenthesis_opener'];
$scope_end = $condition['parenthesis_closer'];
} else {
/*
* We are more loose, requiring only that the variable be validated
* in the same function/file scope as it is used.
*/
$scope_start = 0;
/*
* Check if we are in a function.
*
* Note: PHP 7.4+ arrow functions are not taken into account as those are not
* included in the "conditions" array. Additionally, arrow functions have
* access to variables outside their direct scope.
*/
$function = Conditions::getLastCondition( $phpcsFile, $stackPtr, array( \T_FUNCTION, \T_CLOSURE ) );
// If so, we check only within the function, otherwise the whole file.
if ( false !== $function ) {
$scope_start = $tokens[ $function ]['scope_opener'];
}
$scope_end = $stackPtr;
}
if ( ! empty( $array_keys ) && ! is_array( $array_keys ) ) {
$array_keys = (array) $array_keys;
}
$bare_array_keys = self::strip_quotes_from_array_values( $array_keys );
// phpcs:ignore Generic.CodeAnalysis.JumbledIncrementer.Found -- On purpose, see below.
for ( $i = ( $scope_start + 1 ); $i < $scope_end; $i++ ) {
if ( isset( Collections::closedScopes()[ $tokens[ $i ]['code'] ] )
&& isset( $tokens[ $i ]['scope_closer'] )
) {
// Jump over nested closed scopes as validation done within those does not apply.
$i = $tokens[ $i ]['scope_closer'];
continue;
}
if ( \T_FN === $tokens[ $i ]['code']
&& isset( $tokens[ $i ]['scope_closer'] )
&& $tokens[ $i ]['scope_closer'] < $scope_end
) {
// Jump over nested arrow functions as long as the current variable isn't *in* the arrow function.
$i = $tokens[ $i ]['scope_closer'];
continue;
}
if ( isset( self::$targets[ $tokens[ $i ]['code'] ] ) === false ) {
continue;
}
switch ( self::$targets[ $tokens[ $i ]['code'] ] ) {
case 'construct':
$issetOpener = $phpcsFile->findNext( Tokens::$emptyTokens, ( $i + 1 ), null, true );
if ( false === $issetOpener
|| \T_OPEN_PARENTHESIS !== $tokens[ $issetOpener ]['code']
|| isset( $tokens[ $issetOpener ]['parenthesis_closer'] ) === false
) {
// Parse error or live coding.
continue 2;
}
$issetCloser = $tokens[ $issetOpener ]['parenthesis_closer'];
// Look for this variable. We purposely stomp $i from the parent loop.
for ( $i = ( $issetOpener + 1 ); $i < $issetCloser; $i++ ) {
if ( \T_VARIABLE !== $tokens[ $i ]['code'] ) {
continue;
}
if ( $tokens[ $stackPtr ]['content'] !== $tokens[ $i ]['content'] ) {
continue;
}
// If we're checking for specific array keys (ex: 'hello' in
// $_POST['hello']), that must match too. Quote-style, however, doesn't matter.
if ( ! empty( $bare_array_keys ) ) {
$found_keys = VariableHelper::get_array_access_keys( $phpcsFile, $i );
$found_keys = self::strip_quotes_from_array_values( $found_keys );
$diff = array_diff_assoc( $bare_array_keys, $found_keys );
if ( ! empty( $diff ) ) {
continue;
}
}
return true;
}
break;
case 'function_call':
// Only check calls to array_key_exists() and key_exists().
if ( isset( self::$key_exists_functions[ strtolower( $tokens[ $i ]['content'] ) ] ) === false ) {
continue 2;
}
$next_non_empty = $phpcsFile->findNext( Tokens::$emptyTokens, ( $i + 1 ), null, true );
if ( false === $next_non_empty || \T_OPEN_PARENTHESIS !== $tokens[ $next_non_empty ]['code'] ) {
// Not a function call.
continue 2;
}
if ( Context::inAttribute( $phpcsFile, $i ) === true ) {
// Definitely not the function call as those are not allowed in attributes.
continue 2;
}
if ( ContextHelper::has_object_operator_before( $phpcsFile, $i ) === true ) {
// Method call.
continue 2;
}
if ( ContextHelper::is_token_namespaced( $phpcsFile, $i ) === true ) {
// Namespaced function call.
continue 2;
}
$params = PassedParameters::getParameters( $phpcsFile, $i );
// As `key_exists()` is an alias of `array_key_exists()`, the param positions and names are the same.
$array_param = PassedParameters::getParameterFromStack( $params, 2, 'array' );
if ( false === $array_param ) {
continue 2;
}
$array_param_first_token = $phpcsFile->findNext( Tokens::$emptyTokens, $array_param['start'], ( $array_param['end'] + 1 ), true );
if ( false === $array_param_first_token
|| \T_VARIABLE !== $tokens[ $array_param_first_token ]['code']
|| $tokens[ $array_param_first_token ]['content'] !== $tokens[ $stackPtr ]['content']
) {
continue 2;
}
if ( ! empty( $bare_array_keys ) ) {
// Prevent the original array from being altered.
$bare_keys = $bare_array_keys;
$last_key = array_pop( $bare_keys );
/*
* For multi-level array access, the complete set of keys could be split between
* the $key and the $array parameter, but could also be completely in the $array
* parameter, so we need to check both options.
*/
$found_keys = VariableHelper::get_array_access_keys( $phpcsFile, $array_param_first_token );
$found_keys = self::strip_quotes_from_array_values( $found_keys );
// First try matching the complete set against the array parameter.
$diff = array_diff_assoc( $bare_array_keys, $found_keys );
if ( empty( $diff ) ) {
return true;
}
// If that failed, try getting an exact match for the subset against the
// $array parameter and the last key against the first.
$key_param = PassedParameters::getParameterFromStack( $params, 1, 'key' );
if ( false !== $key_param
&& $bare_keys === $found_keys
&& TextStrings::stripQuotes( $key_param['raw'] ) === $last_key
) {
return true;
}
// Didn't find the correct array keys.
continue 2;
}
return true;
case 'coalesce':
$prev = $i;
do {
$prev = $phpcsFile->findPrevious( Tokens::$emptyTokens, ( $prev - 1 ), null, true );
// Skip over array keys, like `$_GET['key']['subkey']`.
if ( \T_CLOSE_SQUARE_BRACKET === $tokens[ $prev ]['code'] ) {
$prev = $tokens[ $prev ]['bracket_opener'];
continue;
}
break;
} while ( $prev >= ( $scope_start + 1 ) );
// We should now have reached the variable.
if ( \T_VARIABLE !== $tokens[ $prev ]['code'] ) {
continue 2;
}
if ( $tokens[ $prev ]['content'] !== $tokens[ $stackPtr ]['content'] ) {
continue 2;
}
if ( ! empty( $bare_array_keys ) ) {
$found_keys = VariableHelper::get_array_access_keys( $phpcsFile, $prev );
$found_keys = self::strip_quotes_from_array_values( $found_keys );
$diff = array_diff_assoc( $bare_array_keys, $found_keys );
if ( ! empty( $diff ) ) {
continue 2;
}
}
// Right variable, correct key.
return true;
}
}
return false;
}
/**
* Strip quotes of all the values in an array containing only text strings.
*
* @since 3.0.0
*
* @param string[] $text_strings The input array.
*
* @return string[]
*/
private static function strip_quotes_from_array_values( array $text_strings ) {
return array_map( array( 'PHPCSUtils\Utils\TextStrings', 'stripQuotes' ), $text_strings );
}
}