*/ protected $unsafePrintingFunctions = array( '_e' => array( 'alternative' => 'esc_html_e() or esc_attr_e()', 'params' => array( 1 => 'text', ), ), '_ex' => array( 'alternative' => 'echo esc_html_x() or echo esc_attr_x()', 'params' => array( 1 => 'text', ), ), ); /** * List of names of the native PHP constants which can be considered safe. * * @since 1.0.0 * * @var array */ private $safe_php_constants = array( 'PHP_EOL' => true, // String. 'PHP_VERSION' => true, // Integer. 'PHP_MAJOR_VERSION' => true, // Integer. 'PHP_MINOR_VERSION' => true, // Integer. 'PHP_RELEASE_VERSION' => true, // Integer. 'PHP_VERSION_ID' => true, // Integer. 'PHP_EXTRA_VERSION' => true, // String. 'PHP_DEBUG' => true, // Integer. ); /** * List of tokens which can be considered as safe when directly part of the output. * * This list is enhanced with additional tokens in the `register()` method. * * @since 0.12.0 * * @var array */ private $safe_components = array( \T_LNUMBER => \T_LNUMBER, \T_DNUMBER => \T_DNUMBER, \T_TRUE => \T_TRUE, \T_FALSE => \T_FALSE, \T_NULL => \T_NULL, \T_CONSTANT_ENCAPSED_STRING => \T_CONSTANT_ENCAPSED_STRING, \T_START_NOWDOC => \T_START_NOWDOC, \T_NOWDOC => \T_NOWDOC, \T_END_NOWDOC => \T_END_NOWDOC, \T_BOOLEAN_NOT => \T_BOOLEAN_NOT, ); /** * List of keyword tokens this sniff listens for, which can also be used as an inline expression. * * @since 3.0.0 * * @var array */ private $target_keywords = array( \T_EXIT => \T_EXIT, \T_PRINT => \T_PRINT, \T_THROW => \T_THROW, ); /** * Returns an array of tokens this test wants to listen for. * * @return string|int[] */ public function register() { // Enrich the list of "safe components" tokens. $this->safe_components += Tokens::$comparisonTokens; $this->safe_components += Tokens::$operators; $this->safe_components += Tokens::$booleanOperators; $this->safe_components += Collections::incrementDecrementOperators(); // Set up the tokens the sniff should listen too. $targets = array_merge( parent::register(), $this->target_keywords ); $targets[] = \T_ECHO; $targets[] = \T_OPEN_TAG_WITH_ECHO; return $targets; } /** * Groups of functions this sniff is looking for. * * @since 3.0.0 * * @return array */ public function getGroups() { // Make sure all array keys are lowercase (could contain user provided function names). $printing_functions = array_change_key_case( $this->get_printing_functions(), \CASE_LOWER ); // Remove the unsafe printing functions to prevent duplicate notices. $printing_functions = array_diff_key( $printing_functions, $this->unsafePrintingFunctions ); return array( 'unsafe_printing_functions' => array( 'functions' => array_keys( $this->unsafePrintingFunctions ), ), 'printing_functions' => array( 'functions' => array_keys( $printing_functions ), ), ); } /** * Processes this test, when one of its tokens is encountered. * * @since 3.0.0 This method has been split up. * * @param int $stackPtr The position of the current token in the stack. * * @return int|void Integer stack pointer to skip forward or void to continue * normal file processing. */ public function process_token( $stackPtr ) { $start = ( $stackPtr + 1 ); $end = $start; switch ( $this->tokens[ $stackPtr ]['code'] ) { case \T_STRING: // Prevent exclusion of any of the function groups. $this->exclude = array(); // In the tests, custom printing functions may be added/removed on the fly. if ( defined( 'PHP_CODESNIFFER_IN_TESTS' ) ) { $this->setup_groups( 'functions' ); } // Let the abstract parent class handle the initial function call check. return parent::process_token( $stackPtr ); case \T_EXIT: $next_non_empty = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true ); if ( false === $next_non_empty || \T_OPEN_PARENTHESIS !== $this->tokens[ $next_non_empty ]['code'] || isset( $this->tokens[ $next_non_empty ]['parenthesis_closer'] ) === false ) { // Live coding/parse error or an exit/die which doesn't pass a status code. Ignore. return; } // $end is not examined, so make sure the parentheses are balanced. $start = $next_non_empty; $end = ( $this->tokens[ $next_non_empty ]['parenthesis_closer'] + 1 ); break; case \T_THROW: // Find the open parentheses, while stepping over the exception creation tokens. $ignore = Tokens::$emptyTokens; $ignore += Collections::namespacedNameTokens(); $ignore += Collections::functionCallTokens(); $ignore += Collections::objectOperators(); $next_relevant = $this->phpcsFile->findNext( $ignore, ( $stackPtr + 1 ), null, true ); if ( false === $next_relevant ) { return; } if ( \T_NEW === $this->tokens[ $next_relevant ]['code'] ) { $next_relevant = $this->phpcsFile->findNext( $ignore, ( $next_relevant + 1 ), null, true ); if ( false === $next_relevant ) { return; } } if ( \T_OPEN_PARENTHESIS !== $this->tokens[ $next_relevant ]['code'] || isset( $this->tokens[ $next_relevant ]['parenthesis_closer'] ) === false ) { // Live codind/parse error or a pre-created exception. Nothing to do for us. return; } $end = $this->tokens[ $next_relevant ]['parenthesis_closer']; // Check if the throw is within a `try-catch`. // Doing this here (instead of earlier) to allow skipping to the end of the statement. $search_for = Collections::closedScopes(); $search_for[ \T_TRY ] = \T_TRY; $last_condition = Conditions::getLastCondition( $this->phpcsFile, $stackPtr, $search_for ); if ( false !== $last_condition && \T_TRY === $this->tokens[ $last_condition ]['code'] ) { // This exception will (probably) be caught, so ignore it. return $end; } $call_token = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $next_relevant - 1 ), null, true ); $params = PassedParameters::getParameters( $this->phpcsFile, $call_token ); if ( empty( $params ) ) { // No parameters passed, nothing to do. return $end; } // Examine each parameter individually. foreach ( $params as $param ) { $this->check_code_is_escaped( $param['start'], ( $param['end'] + 1 ), 'ExceptionNotEscaped' ); } return $end; case \T_PRINT: $end = BCFile::findEndOfStatement( $this->phpcsFile, $stackPtr ); if ( \T_COMMA !== $this->tokens[ $end ]['code'] && \T_SEMICOLON !== $this->tokens[ $end ]['code'] && \T_COLON !== $this->tokens[ $end ]['code'] && \T_DOUBLE_ARROW !== $this->tokens[ $end ]['code'] && isset( $this->tokens[ ( $end + 1 ) ] ) ) { /* * FindEndOfStatement includes a comma/(semi-)colon/double arrow if that's the end of * the statement, but for everything else, it returns the last non-empty token _before_ * the end, which would mean the last non-empty token in the statement would not * be examined. Let's fix that. */ ++$end; } // Note: no need to check for close tag as close tag will have the token before the tag as the $end. if ( $end >= ( $this->phpcsFile->numTokens - 1 ) ) { $last_non_empty = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, $end, null, true ); if ( \T_SEMICOLON !== $this->tokens[ $last_non_empty ]['code'] ) { // Live coding/parse error at end of file. Ignore. return; } } // Special case for a print statement *within* a ternary, where we need to find the "inline else" as the end token. $prev_non_empty = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $stackPtr - 1 ), null, true ); if ( \T_INLINE_THEN === $this->tokens[ $prev_non_empty ]['code'] ) { $target_nesting_level = 0; if ( empty( $this->tokens[ $stackPtr ]['nested_parenthesis'] ) === false ) { $target_nesting_level = \count( $this->tokens[ $stackPtr ]['nested_parenthesis'] ); } $inline_else = false; for ( $i = ( $stackPtr + 1 ); $i < $end; $i++ ) { if ( \T_INLINE_ELSE !== $this->tokens[ $i ]['code'] ) { continue; } if ( empty( $this->tokens[ $i ]['nested_parenthesis'] ) && 0 === $target_nesting_level ) { $inline_else = $i; break; } if ( empty( $this->tokens[ $i ]['nested_parenthesis'] ) === false && \count( $this->tokens[ $i ]['nested_parenthesis'] ) === $target_nesting_level ) { $inline_else = $i; break; } } if ( false === $inline_else ) { // Live coding/parse error. Bow out. return; } $end = $inline_else; } break; // Echo, open tag with echo. default: $end = $this->phpcsFile->findNext( array( \T_SEMICOLON, \T_CLOSE_TAG ), $stackPtr ); if ( false === $end ) { // Live coding/parse error. Bow out. return; } break; } return $this->check_code_is_escaped( $start, $end ); } /** * Process a matched function call token. * * @since 3.0.0 Split off from the process_token() method. * * @param int $stackPtr The position of the current token in the stack. * @param string $group_name The name of the group which was matched. * @param string $matched_content The token content (function name) which was matched * in lowercase. * * @return int|void Integer stack pointer to skip forward or void to continue * normal file processing. */ public function process_matched_token( $stackPtr, $group_name, $matched_content ) { // Make sure we only deal with actual function calls, not function import use statements. $next_non_empty = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true ); if ( false === $next_non_empty || \T_OPEN_PARENTHESIS !== $this->tokens[ $next_non_empty ]['code'] || isset( $this->tokens[ $next_non_empty ]['parenthesis_closer'] ) === false ) { // Live coding, parse error or not a function _call_. return; } $end = $this->tokens[ $next_non_empty ]['parenthesis_closer']; if ( 'unsafe_printing_functions' === $group_name ) { $error = $this->phpcsFile->addError( "All output should be run through an escaping function (like %s), found '%s'.", $stackPtr, 'UnsafePrintingFunction', array( $this->unsafePrintingFunctions[ $matched_content ]['alternative'], $matched_content ) ); // If the error was reported, don't bother checking the function's arguments. if ( $error || empty( $this->unsafePrintingFunctions[ $matched_content ]['params'] ) ) { return $end; } // If the function was not reported for being unsafe, examine the relevant parameters. $params = PassedParameters::getParameters( $this->phpcsFile, $stackPtr ); foreach ( $this->unsafePrintingFunctions[ $matched_content ]['params'] as $position => $name ) { $param = PassedParameters::getParameterFromStack( $params, $position, $name ); if ( false === $param ) { // Parameter doesn't exist. Nothing to do. continue; } $this->check_code_is_escaped( $param['start'], ( $param['end'] + 1 ) ); } return $end; } $params = PassedParameters::getParameters( $this->phpcsFile, $stackPtr ); /* * These functions only need to have their first argument - `$message` - escaped. * Note: user_error() is an alias for trigger_error(), so the param names are the same. */ if ( 'trigger_error' === $matched_content || 'user_error' === $matched_content ) { $message_param = PassedParameters::getParameterFromStack( $params, 1, 'message' ); if ( false === $message_param ) { // Message parameter doesn't exist. Nothing to do. return $end; } return $this->check_code_is_escaped( $message_param['start'], ( $message_param['end'] + 1 ) ); } /* * If the first param to `_deprecated_file()` - `$file` - follows the typical `basename( __FILE__ )` * pattern, it doesn't need to be escaped. */ if ( '_deprecated_file' === $matched_content ) { $file_param = PassedParameters::getParameterFromStack( $params, 1, 'file' ); if ( false !== $file_param ) { // Check for a particular code pattern which can safely be ignored. if ( preg_match( '`^[\\\\]?basename\s*\(\s*__FILE__\s*\)$`', $file_param['clean'] ) === 1 ) { unset( $params[1], $params['file'] ); // Remove the param, whether passed positionally or named. } } unset( $file_param ); } // Examine each parameter individually. foreach ( $params as $param ) { $this->check_code_is_escaped( $param['start'], ( $param['end'] + 1 ) ); } return $end; } /** * Check whether each relevant part of an arbitrary group of token is output escaped. * * @since 3.0.0 Split off from the process_token() method. * * @param int $start The position to start checking from. * @param int $end The position to stop the check at. * @param string $code Code to use for the PHPCS error. * * @return int Integer stack pointer to skip forward. */ protected function check_code_is_escaped( $start, $end, $code = 'OutputNotEscaped' ) { /* * Check for a ternary operator. * We only need to do this here if this statement is lacking parenthesis. * Otherwise it will be handled in the below loop. */ $ternary = false; $next_non_empty = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $start + 1 ), null, true ); $last_non_empty = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $end - 1 ), null, true ); if ( \T_OPEN_PARENTHESIS !== $this->tokens[ $next_non_empty ]['code'] || \T_CLOSE_PARENTHESIS !== $this->tokens[ $last_non_empty ]['code'] || ( \T_OPEN_PARENTHESIS === $this->tokens[ $next_non_empty ]['code'] && \T_CLOSE_PARENTHESIS === $this->tokens[ $last_non_empty ]['code'] && isset( $this->tokens[ $next_non_empty ]['parenthesis_closer'] ) && $this->tokens[ $next_non_empty ]['parenthesis_closer'] !== $last_non_empty ) ) { // If there is a (long) ternary skip over the part before the ?. $ternary = $this->find_long_ternary( $start, $end ); if ( false !== $ternary ) { $start = ( $ternary + 1 ); } } $in_cast = false; $watch = true; // Looping through echo'd components. for ( $i = $start; $i < $end; $i++ ) { // Ignore whitespaces and comments. if ( isset( Tokens::$emptyTokens[ $this->tokens[ $i ]['code'] ] ) ) { continue; } // Skip over irrelevant tokens. if ( isset( Tokens::$magicConstants[ $this->tokens[ $i ]['code'] ] ) // Magic constants for debug functions. || \T_NS_SEPARATOR === $this->tokens[ $i ]['code'] || \T_DOUBLE_ARROW === $this->tokens[ $i ]['code'] || \T_CLOSE_PARENTHESIS === $this->tokens[ $i ]['code'] ) { continue; } if ( \T_OPEN_PARENTHESIS === $this->tokens[ $i ]['code'] ) { if ( ! isset( $this->tokens[ $i ]['parenthesis_closer'] ) ) { // Live coding or parse error. break; } if ( $in_cast ) { // Skip to the end of a function call if it has been casted to a safe value. $i = $this->tokens[ $i ]['parenthesis_closer']; $in_cast = false; } else { // Skip over the condition part of a (long) ternary (i.e., to after the ?). $ternary = $this->find_long_ternary( ( $i + 1 ), $this->tokens[ $i ]['parenthesis_closer'] ); if ( false !== $ternary ) { $i = $ternary; } } continue; } /* * If a keyword is encountered in an inline expression and the keyword is one * this sniff listens to, recurse into the sniff, handle the expression * based on the keyword and skip over the code examined. */ if ( isset( $this->target_keywords[ $this->tokens[ $i ]['code'] ] ) ) { $return_value = $this->process_token( $i ); if ( isset( $return_value ) ) { $i = $return_value; } continue; } // Handle PHP 8.0+ match expressions. if ( \T_MATCH === $this->tokens[ $i ]['code'] ) { $match_valid = $this->walk_match_expression( $i, $code ); if ( false === $match_valid ) { // Live coding or parse error. Shouldn't be possible as PHP[CS] will tokenize the keyword as `T_STRING` in that case. break; // @codeCoverageIgnore } $i = $match_valid; continue; } // Examine the items in an array individually for array parameters. if ( isset( Collections::arrayOpenTokensBC()[ $this->tokens[ $i ]['code'] ] ) ) { $array_open_close = Arrays::getOpenClose( $this->phpcsFile, $i ); if ( false === $array_open_close ) { // Short list or misidentified short array token. continue; } $array_items = PassedParameters::getParameters( $this->phpcsFile, $i, 0, true ); if ( ! empty( $array_items ) ) { foreach ( $array_items as $array_item ) { $this->check_code_is_escaped( $array_item['start'], ( $array_item['end'] + 1 ), $code ); } } $i = $array_open_close['closer']; continue; } // Ignore safe PHP native constants. if ( \T_STRING === $this->tokens[ $i ]['code'] && isset( $this->safe_php_constants[ $this->tokens[ $i ]['content'] ] ) && ConstantsHelper::is_use_of_global_constant( $this->phpcsFile, $i ) ) { continue; } // Wake up on concatenation characters, another part to check. if ( \T_STRING_CONCAT === $this->tokens[ $i ]['code'] ) { $watch = true; continue; } // Wake up after a ternary else (:). if ( false !== $ternary && \T_INLINE_ELSE === $this->tokens[ $i ]['code'] ) { $watch = true; continue; } // Wake up for commas. if ( \T_COMMA === $this->tokens[ $i ]['code'] ) { $in_cast = false; $watch = true; continue; } if ( false === $watch ) { continue; } // Allow T_CONSTANT_ENCAPSED_STRING eg: echo 'Some String'; // Also T_LNUMBER, e.g.: echo 45; exit -1; and booleans. if ( isset( $this->safe_components[ $this->tokens[ $i ]['code'] ] ) ) { continue; } // Check for use of *::class. if ( \T_STRING === $this->tokens[ $i ]['code'] || \T_VARIABLE === $this->tokens[ $i ]['code'] || isset( Collections::ooHierarchyKeywords()[ $this->tokens[ $i ]['code'] ] ) ) { $double_colon = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $i + 1 ), $end, true ); if ( false !== $double_colon && \T_DOUBLE_COLON === $this->tokens[ $double_colon ]['code'] ) { $class_keyword = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $double_colon + 1 ), $end, true ); if ( false !== $class_keyword && \T_STRING === $this->tokens[ $class_keyword ]['code'] && 'class' === strtolower( $this->tokens[ $class_keyword ]['content'] ) ) { $i = $class_keyword; continue; } } } $watch = false; // Allow int/double/bool casted variables. if ( isset( ContextHelper::get_safe_cast_tokens()[ $this->tokens[ $i ]['code'] ] ) ) { /* * If the next thing is a match expression, skip over it as whatever is * being returned will be safe casted. * Do not set `$in_cast` to `true`. */ $next_non_empty = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $i + 1 ), $end, true ); if ( false !== $next_non_empty && \T_MATCH === $this->tokens[ $next_non_empty ]['code'] && isset( $this->tokens[ $next_non_empty ]['scope_closer'] ) ) { $i = $this->tokens[ $next_non_empty ]['scope_closer']; continue; } $in_cast = true; continue; } // Handle heredocs separately as they only need escaping when interpolation is used. if ( \T_START_HEREDOC === $this->tokens[ $i ]['code'] ) { $current = ( $i + 1 ); while ( isset( $this->tokens[ $current ] ) && \T_HEREDOC === $this->tokens[ $current ]['code'] ) { $embeds = TextStrings::getEmbeds( $this->tokens[ $current ]['content'] ); if ( ! empty( $embeds ) ) { $this->phpcsFile->addError( 'All output should be run through an escaping function (see the Security sections in the WordPress Developer Handbooks), found interpolation in unescaped heredoc.', $current, 'HeredocOutputNotEscaped' ); } ++$current; } $i = $current; continue; } // Now check that next token is a function call. if ( \T_STRING === $this->tokens[ $i ]['code'] ) { $ptr = $i; $functionName = $this->tokens[ $i ]['content']; $function_opener = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $i + 1 ), null, true ); $is_formatting_function = FormattingFunctionsHelper::is_formatting_function( $functionName ); if ( false !== $function_opener && \T_OPEN_PARENTHESIS === $this->tokens[ $function_opener ]['code'] ) { if ( ArrayWalkingFunctionsHelper::is_array_walking_function( $functionName ) ) { // Get the callback parameter. $callback = ArrayWalkingFunctionsHelper::get_callback_parameter( $this->phpcsFile, $ptr ); if ( ! empty( $callback ) ) { /* * If this is a function callback (not a method callback array) and we're able * to resolve the function name, do so. */ $mapped_function = $this->phpcsFile->findNext( Tokens::$emptyTokens, $callback['start'], ( $callback['end'] + 1 ), true ); if ( false !== $mapped_function && \T_CONSTANT_ENCAPSED_STRING === $this->tokens[ $mapped_function ]['code'] ) { $functionName = TextStrings::stripQuotes( $this->tokens[ $mapped_function ]['content'] ); $ptr = $mapped_function; } } } // If this is a formatting function, we examine the parameters individually. if ( $is_formatting_function ) { $formatting_params = PassedParameters::getParameters( $this->phpcsFile, $i ); if ( ! empty( $formatting_params ) ) { foreach ( $formatting_params as $format_param ) { $this->check_code_is_escaped( $format_param['start'], ( $format_param['end'] + 1 ), $code ); } } $watch = true; } // Skip pointer to after the function. if ( isset( $this->tokens[ $function_opener ]['parenthesis_closer'] ) ) { $i = $this->tokens[ $function_opener ]['parenthesis_closer']; } else { // Live coding or parse error. break; } } // If this is a safe function, we don't flag it. if ( $is_formatting_function || $this->is_escaping_function( $functionName ) || $this->is_auto_escaped_function( $functionName ) ) { // Special case get_search_query() which is unsafe if $escaped = false. if ( 'get_search_query' === strtolower( $functionName ) ) { $escaped_param = PassedParameters::getParameter( $this->phpcsFile, $ptr, 1, 'escaped' ); if ( false !== $escaped_param && 'true' !== $escaped_param['clean'] ) { $this->phpcsFile->addError( 'Output from get_search_query() is unsafe due to $escaped parameter being set to "false".', $ptr, 'UnsafeSearchQuery' ); } } continue; } $content = $functionName; } else { $content = $this->tokens[ $i ]['content']; $ptr = $i; } // Make the error message a little more informative for array access variables. if ( \T_VARIABLE === $this->tokens[ $ptr ]['code'] ) { $array_keys = VariableHelper::get_array_access_keys( $this->phpcsFile, $ptr ); if ( ! empty( $array_keys ) ) { $content .= '[' . implode( '][', $array_keys ) . ']'; } } $this->phpcsFile->addError( "All output should be run through an escaping function (see the Security sections in the WordPress Developer Handbooks), found '%s'.", $ptr, $code, array( $content ) ); } return $end; } /** * Check whether there is a ternary token at the right nesting level in an arbitrary set of tokens. * * @since 3.0.0 Split off from the process_token() method. * * @param int $start The position to start checking from. * @param int $end The position to stop the check at. * * @return int|false Stack pointer to the ternary or FALSE if no ternary was found or * if this is a short ternary. */ private function find_long_ternary( $start, $end ) { for ( $i = $start; $i < $end; $i++ ) { // Ignore anything within square brackets. if ( isset( $this->tokens[ $i ]['bracket_opener'], $this->tokens[ $i ]['bracket_closer'] ) && $i === $this->tokens[ $i ]['bracket_opener'] ) { $i = $this->tokens[ $i ]['bracket_closer']; continue; } // Skip past nested arrays, function calls and arbitrary groupings. if ( \T_OPEN_PARENTHESIS === $this->tokens[ $i ]['code'] && isset( $this->tokens[ $i ]['parenthesis_closer'] ) ) { $i = $this->tokens[ $i ]['parenthesis_closer']; continue; } // Skip past closures, anonymous classes and anything else scope related. if ( isset( $this->tokens[ $i ]['scope_condition'], $this->tokens[ $i ]['scope_closer'] ) && $this->tokens[ $i ]['scope_condition'] === $i ) { $i = $this->tokens[ $i ]['scope_closer']; continue; } if ( \T_INLINE_THEN !== $this->tokens[ $i ]['code'] ) { continue; } /* * Okay, we found a ternary and it should be at the correct nesting level. * If this is a short ternary, it shouldn't be ignored though. */ if ( Operators::isShortTernary( $this->phpcsFile, $i ) === true ) { return false; } return $i; } return false; } /** * Examine a match expression and only check for escaping in the "returned" parts of the match expression. * * {@internal PHPCSUtils will likely contain a utility for parsing match expressions in the future. * Ref: https://github.com/PHPCSStandards/PHPCSUtils/issues/497} * * @since 3.0.0 * * @param int $stackPtr Pointer to a T_MATCH token. * @param string $code Code to use for the PHPCS error. * * @return int|false Stack pointer to skip to or FALSE if the match expression contained a parse error. */ private function walk_match_expression( $stackPtr, $code ) { if ( ! isset( $this->tokens[ $stackPtr ]['scope_opener'], $this->tokens[ $stackPtr ]['scope_closer'] ) ) { // Parse error/live coding. Shouldn't be possible as PHP[CS] will tokenize the keyword as `T_STRING` in that case. return false; // @codeCoverageIgnore } $current = $this->tokens[ $stackPtr ]['scope_opener']; $end = $this->tokens[ $stackPtr ]['scope_closer']; do { $current = $this->phpcsFile->findNext( \T_MATCH_ARROW, ( $current + 1 ), $end ); if ( false === $current ) { // We must have reached the last match item (or there is a parse error). break; } $item_start = ( $current + 1 ); $item_end = false; // Find the first comma at the same level. for ( $i = $item_start; $i <= $end; $i++ ) { // Ignore anything within square brackets. if ( isset( $this->tokens[ $i ]['bracket_opener'], $this->tokens[ $i ]['bracket_closer'] ) && $i === $this->tokens[ $i ]['bracket_opener'] ) { $i = $this->tokens[ $i ]['bracket_closer']; continue; } // Skip past nested arrays, function calls and arbitrary groupings. if ( \T_OPEN_PARENTHESIS === $this->tokens[ $i ]['code'] && isset( $this->tokens[ $i ]['parenthesis_closer'] ) ) { $i = $this->tokens[ $i ]['parenthesis_closer']; continue; } // Skip past closures, anonymous classes and anything else scope related. if ( isset( $this->tokens[ $i ]['scope_condition'], $this->tokens[ $i ]['scope_closer'] ) && $this->tokens[ $i ]['scope_condition'] === $i ) { $i = $this->tokens[ $i ]['scope_closer']; continue; } if ( \T_COMMA !== $this->tokens[ $i ]['code'] && $i !== $end ) { continue; } $item_end = $i; break; } if ( false === $item_end ) { // Parse error/live coding. Shouldn't be possible. return false; // @codeCoverageIgnore } // Now check that the value returned by this match "leaf" is correctly escaped. $this->check_code_is_escaped( $item_start, $item_end, $code ); // Independently of whether or not the check was succesfull or ran into (parse error) problems, // always skip to the identified end of the item. $current = $item_end; } while ( $current < $end ); return $end; } }