@@ -1814,38 +1814,80 @@ public function add_unslash_error( $stackPtr ) {
18141814 );
18151815 }
18161816
1817+ /**
1818+ * Get the index keys of an array variable.
1819+ *
1820+ * E.g., "bar" and "baz" in $foo['bar']['baz'].
1821+ *
1822+ * @since 2.1.0
1823+ *
1824+ * @param int $stackPtr The index of the variable token in the stack.
1825+ * @param bool $all Whether to get all keys or only the first.
1826+ * Defaults to `true`(= all).
1827+ *
1828+ * @return array An array of index keys whose value is being accessed.
1829+ * or an empty array if this is not array access.
1830+ */
1831+ protected function get_array_access_keys ( $ stackPtr , $ all = true ) {
1832+
1833+ $ keys = array ();
1834+
1835+ if ( \T_VARIABLE !== $ this ->tokens [ $ stackPtr ]['code ' ] ) {
1836+ return $ keys ;
1837+ }
1838+
1839+ $ current = $ stackPtr ;
1840+
1841+ do {
1842+ // Find the next non-empty token.
1843+ $ open_bracket = $ this ->phpcsFile ->findNext (
1844+ Tokens::$ emptyTokens ,
1845+ ( $ current + 1 ),
1846+ null ,
1847+ true
1848+ );
1849+
1850+ // If it isn't a bracket, this isn't an array-access.
1851+ if ( false === $ open_bracket
1852+ || \T_OPEN_SQUARE_BRACKET !== $ this ->tokens [ $ open_bracket ]['code ' ]
1853+ || ! isset ( $ this ->tokens [ $ open_bracket ]['bracket_closer ' ] )
1854+ ) {
1855+ break ;
1856+ }
1857+
1858+ $ key = $ this ->phpcsFile ->getTokensAsString (
1859+ ( $ open_bracket + 1 ),
1860+ ( $ this ->tokens [ $ open_bracket ]['bracket_closer ' ] - $ open_bracket - 1 )
1861+ );
1862+
1863+ $ keys [] = trim ( $ key );
1864+ $ current = $ this ->tokens [ $ open_bracket ]['bracket_closer ' ];
1865+ } while ( isset ( $ this ->tokens [ $ current ] ) && true === $ all );
1866+
1867+ return $ keys ;
1868+ }
1869+
18171870 /**
18181871 * Get the index key of an array variable.
18191872 *
18201873 * E.g., "bar" in $foo['bar'].
18211874 *
18221875 * @since 0.5.0
1876+ * @since 2.1.0 Now uses get_array_access_keys() under the hood.
18231877 *
18241878 * @param int $stackPtr The index of the token in the stack.
18251879 *
18261880 * @return string|false The array index key whose value is being accessed.
18271881 */
18281882 protected function get_array_access_key ( $ stackPtr ) {
18291883
1830- // Find the next non-empty token.
1831- $ open_bracket = $ this ->phpcsFile ->findNext (
1832- Tokens::$ emptyTokens ,
1833- ( $ stackPtr + 1 ),
1834- null ,
1835- true
1836- );
1884+ $ keys = $ this ->get_array_access_keys ( $ stackPtr , false );
18371885
1838- // If it isn't a bracket, this isn't an array-access.
1839- if ( false === $ open_bracket || \T_OPEN_SQUARE_BRACKET !== $ this ->tokens [ $ open_bracket ]['code ' ] ) {
1840- return false ;
1886+ if ( isset ( $ keys [0 ] ) ) {
1887+ return $ keys [0 ];
18411888 }
18421889
1843- $ key = $ this ->phpcsFile ->getTokensAsString (
1844- ( $ open_bracket + 1 ),
1845- ( $ this ->tokens [ $ open_bracket ]['bracket_closer ' ] - $ open_bracket - 1 )
1846- );
1847-
1848- return trim ( $ key );
1890+ return false ;
18491891 }
18501892
18511893 /**
@@ -1874,17 +1916,20 @@ protected function get_array_access_key( $stackPtr ) {
18741916 *
18751917 * @since 0.5.0
18761918 * @since 2.1.0 Now recognizes array_key_exists() and key_exists() as validation functions.
1919+ * @since 2.1.0 Stricter check on whether the correct variable and the correct
1920+ * array keys are being validated.
18771921 *
1878- * @param int $stackPtr The index of this token in the stack.
1879- * @param string $array_key An array key to check for ("bar" in $foo['bar']).
1880- * @param bool $in_condition_only Whether to require that this use of the
1881- * variable occur within the scope of the
1882- * validating condition, or just in the same
1883- * scope as it (default).
1922+ * @param int $stackPtr The index of this token in the stack.
1923+ * @param array|string $array_keys An array key to check for ("bar" in $foo['bar'])
1924+ * or an array of keys for multi-level array access.
1925+ * @param bool $in_condition_only Whether to require that this use of the
1926+ * variable occur within the scope of the
1927+ * validating condition, or just in the same
1928+ * scope as it (default).
18841929 *
18851930 * @return bool Whether the var is validated.
18861931 */
1887- protected function is_validated ( $ stackPtr , $ array_key = null , $ in_condition_only = false ) {
1932+ protected function is_validated ( $ stackPtr , $ array_keys = array () , $ in_condition_only = false ) {
18881933
18891934 if ( $ in_condition_only ) {
18901935 /*
@@ -1935,11 +1980,14 @@ protected function is_validated( $stackPtr, $array_key = null, $in_condition_onl
19351980 }
19361981
19371982 $ scope_end = $ stackPtr ;
1983+ }
19381984
1985+ if ( ! empty ( $ array_keys ) && ! is_array ( $ array_keys ) ) {
1986+ $ array_keys = (array ) $ array_keys ;
19391987 }
19401988
1941- $ bare_array_key = $ this -> strip_quotes ( $ array_key );
1942- $ targets = array (
1989+ $ bare_array_keys = array_map ( array ( $ this , ' strip_quotes ' ), $ array_keys );
1990+ $ targets = array (
19431991 \T_ISSET => 'construct ' ,
19441992 \T_EMPTY => 'construct ' ,
19451993 \T_UNSET => 'construct ' ,
@@ -1974,11 +2022,15 @@ protected function is_validated( $stackPtr, $array_key = null, $in_condition_onl
19742022 continue ;
19752023 }
19762024
1977- // If we're checking for a specific array key (ex: 'hello' in
2025+ // If we're checking for specific array keys (ex: 'hello' in
19782026 // $_POST['hello']), that must match too. Quote-style, however, doesn't matter.
1979- if ( isset ( $ array_key )
1980- && $ this ->strip_quotes ( $ this ->get_array_access_key ( $ i ) ) !== $ bare_array_key ) {
1981- continue ;
2027+ if ( ! empty ( $ bare_array_keys ) ) {
2028+ $ found_keys = $ this ->get_array_access_keys ( $ i );
2029+ $ found_keys = array_map ( array ( $ this , 'strip_quotes ' ), $ found_keys );
2030+ $ diff = array_diff_assoc ( $ bare_array_keys , $ found_keys );
2031+ if ( ! empty ( $ diff ) ) {
2032+ continue ;
2033+ }
19822034 }
19832035
19842036 return true ;
@@ -2011,12 +2063,45 @@ protected function is_validated( $stackPtr, $array_key = null, $in_condition_onl
20112063 }
20122064
20132065 $ params = $ this ->get_function_call_parameters ( $ i );
2014- if ( $ params [2 ]['raw ' ] !== $ this ->tokens [ $ stackPtr ]['content ' ] ) {
2066+ if ( count ( $ params ) < 2 ) {
2067+ continue 2 ;
2068+ }
2069+
2070+ $ param2_first_token = $ this ->phpcsFile ->findNext ( Tokens::$ emptyTokens , $ params [2 ]['start ' ], ( $ params [2 ]['end ' ] + 1 ), true );
2071+ if ( false === $ param2_first_token
2072+ || \T_VARIABLE !== $ this ->tokens [ $ param2_first_token ]['code ' ]
2073+ || $ this ->tokens [ $ param2_first_token ]['content ' ] !== $ this ->tokens [ $ stackPtr ]['content ' ]
2074+ ) {
20152075 continue 2 ;
20162076 }
20172077
2018- if ( isset ( $ array_key )
2019- && $ this ->strip_quotes ( $ params [1 ]['raw ' ] ) !== $ bare_array_key ) {
2078+ if ( ! empty ( $ bare_array_keys ) ) {
2079+ // Prevent the original array from being altered.
2080+ $ bare_keys = $ bare_array_keys ;
2081+ $ last_key = array_pop ( $ bare_keys );
2082+
2083+ /*
2084+ * For multi-level array access, the complete set of keys could be split between
2085+ * the first and the second parameter, but could also be completely in the second
2086+ * parameter, so we need to check both options.
2087+ */
2088+
2089+ $ found_keys = $ this ->get_array_access_keys ( $ param2_first_token );
2090+ $ found_keys = array_map ( array ( $ this , 'strip_quotes ' ), $ found_keys );
2091+
2092+ // First try matching the complete set against the second parameter.
2093+ $ diff = array_diff_assoc ( $ bare_array_keys , $ found_keys );
2094+ if ( empty ( $ diff ) ) {
2095+ return true ;
2096+ }
2097+
2098+ // If that failed, try getting an exact match for the subset against the
2099+ // second parameter and the last key against the first.
2100+ if ( $ bare_keys === $ found_keys && $ this ->strip_quotes ( $ params [1 ]['raw ' ] ) === $ last_key ) {
2101+ return true ;
2102+ }
2103+
2104+ // Didn't find the correct array keys.
20202105 continue 2 ;
20212106 }
20222107
0 commit comments