diff --git a/.phpcs.xml.dist b/.phpcs.xml.dist index 0bc9d6c10e..ef779c2dc3 100644 --- a/.phpcs.xml.dist +++ b/.phpcs.xml.dist @@ -7,19 +7,19 @@ + + /bin/class-ruleset-test.php */vendor/* - + - - diff --git a/.travis.yml b/.travis.yml index 64fe800947..bb60dd86aa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,3 @@ -sudo: false - dist: trusty cache: @@ -21,7 +19,7 @@ php: - 7.1 - 7.2 - 7.3 - - nightly + - "7.4snapshot" env: # `master` is now 3.x. @@ -42,7 +40,7 @@ matrix: allow_failures: # Allow failures for unstable builds. - - php: nightly + - php: "7.4snapshot" before_install: # Speed up build time by disabling Xdebug. @@ -60,12 +58,20 @@ before_install: # The above require already does the install. $(pwd)/vendor/bin/phpcs --config-set installed_paths $(pwd) fi + # Download PHPUnit 7.x for builds on PHP >= 7.2 as the PHPCS + # test suite is currently not compatible with PHPUnit 8.x. + - if [[ ${TRAVIS_PHP_VERSION:0:3} > "7.1" ]]; then wget -P $PHPUNIT_DIR https://phar.phpunit.de/phpunit-7.phar && chmod +x $PHPUNIT_DIR/phpunit-7.phar; fi script: # Lint the PHP files against parse errors. - if [[ "$LINT" == "1" ]]; then if find . -path ./vendor -prune -o -path ./bin -prune -o -name "*.php" -exec php -l {} \; | grep "^[Parse error|Fatal error]"; then exit 1; fi; fi # Run the unit tests. - - phpunit --filter WordPress --bootstrap="$(pwd)/vendor/squizlabs/php_codesniffer/tests/bootstrap.php" $(pwd)/vendor/squizlabs/php_codesniffer/tests/AllTests.php + - | + if [[ ${TRAVIS_PHP_VERSION:0:3} > "7.1" ]]; then + php $PHPUNIT_DIR/phpunit-7.phar --filter WordPress --bootstrap="$(pwd)/vendor/squizlabs/php_codesniffer/tests/bootstrap.php" $(pwd)/vendor/squizlabs/php_codesniffer/tests/AllTests.php + else + phpunit --filter WordPress --bootstrap="$(pwd)/vendor/squizlabs/php_codesniffer/tests/bootstrap.php" $(pwd)/vendor/squizlabs/php_codesniffer/tests/AllTests.php + fi # Test for fixer conflicts by running the auto-fixers of the complete WPCS over the test case files. # This is not an exhaustive test, but should give an early indication for typical fixer conflicts. # For the first run, the exit code will be 1 (= all fixable errors fixed). diff --git a/CHANGELOG.md b/CHANGELOG.md index 96b8b726e9..6a5e83627a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,82 @@ This projects adheres to [Semantic Versioning](https://semver.org/) and [Keep a _No documentation available about unreleased changes as of yet._ +## [2.1.0] - 2019-04-08 + +### Added +- New `WordPress.PHP.IniSet` sniff to the `WordPress-Extra` ruleset. + This sniff will detect calls to `ini_set()` and `ini_alter()` and warn against their use as changing configuration values at runtime leads to an unpredictable runtime environment, which can result in conflicts between core/plugins/themes. + - The sniff will not throw notices about a very limited set of "safe" ini directives. + - For a number of ini directives for which there are alternative, non-conflicting ways to achieve the same available, the sniff will throw an `error` and advise using the alternative. +- `doubleval()`, `count()` and `sizeof()` to `Sniff::$unslashingSanitizingFunctions` property. + While `count()` and its alias `sizeof()`, don't actually unslash or sanitize, the output of these functions is safe to use without unslashing or sanitizing. + This affects the `WordPress.Security.ValidatedSanitizedInput` and the `WordPress.Security.NonceVerification` sniffs. +- The new WP 5.1 `WP_UnitTestCase_Base` class to the `Sniff::$test_class_whitelist` property. +- New `Sniff::get_array_access_keys()` utility method to retrieve all array keys for a variable using multi-level array access. +- New `Sniff::is_class_object_call()`, `Sniff::is_token_namespaced()` utility methods. + These should help make the checking of whether or not a function call is a global function, method call or a namespaced function call more consistent. + This also implements allowing for the [namespace keyword being used as an operator](https://www.php.net/manual/en/language.namespaces.nsconstants.php#example-258). +- New `Sniff::is_in_function_call()` utility method to facilitate checking whether a token is (part of) a parameter passed to a specific (set of) function(s). +- New `Sniff::is_in_type_test()` utility method to determine if a variable is being type tested, along with a `Sniff::$typeTestFunctions` property containing the names of the functions this applies to. +- New `Sniff::is_in_array_comparison()` utility method to determine if a variable is (part of) a parameter in an array-value comparison, along with a `Sniff::$arrayCompareFunctions` property containing the names of the relevant functions. +- New `Sniff::$arrayWalkingFunctions` property containing the names of array functions which apply a callback to the array, but don't change the array by reference. +- New `Sniff::$unslashingFunctions` property containing the names of functions which unslash data passed to them and return the unslashed result. + +### Changed +- Moved the `WordPress.PHP.StrictComparisons`, `WordPress.PHP.StrictInArray` and the `WordPress.CodeAnalysis.AssignmentInCondition` sniff from the `WordPress-Extra` to the `WordPress-Core` ruleset. +- The `Squiz.Commenting.InlineComment.SpacingAfter` error is no longer included in the `WordPress-Docs` ruleset. +- The default value for `minimum_supported_wp_version`, as used by a [number of sniffs detecting usage of deprecated WP features](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/wiki/Customizable-sniff-properties#minimum-wp-version-to-check-for-usage-of-deprecated-functions-classes-and-function-parameters), has been updated to `4.8`. +- The `WordPress.WP.DeprecatedFunctions` sniff will now detect functions deprecated in WP 5.1. +- The `WordPress.Security.NonceVerification` sniff now allows for variable type testing, comparisons, unslashing and sanitization before the nonce check. A nonce check within the same scope, however, is still required. +- The `WordPress.Security.ValidatedSanitizedInput` sniff now allows for using a superglobal in an array-value comparison without sanitization, same as when the superglobal is used in a scalar value comparison. +- `WordPress.NamingConventions.PrefixAllGlobals`: some of the error messages have been made more explicit. +- The error messages for the `WordPress.Security.ValidatedSanitizedInput` sniff will now contain information on the index keys accessed. +- The error message for the `WordPress.Security.ValidatedSanitizedInput.InputNotValidated` has been reworded to make it more obvious what the actual issue being reported is. +- The error message for the `WordPress.Security.ValidatedSanitizedInput.MissingUnslash` has been reworded. +- The `Sniff::is_comparison()` method now has a new `$include_coalesce` parameter to allow for toggling whether the null coalesce operator should be seen as a comparison operator. Defaults to `true`. +- All sniffs are now also being tested against PHP 7.4 (unstable) for consistent sniff results. +- The recommended version of the suggested DealerDirect PHPCS Composer plugin is now `^0.5.0`. +- Various minor code tweaks and clean up. + +### Removed +- `ini_set` and `ini_alter` from the list of functions detected by the `WordPress.PHP.DiscouragedFunctions` sniff. + These are now covered via the new `WordPress.PHP.IniSet` sniff. +- `in_array()` and `array_key_exists()` from the list of `Sniff::$sanitizingFunctions`. These are now handled differently. + +### Fixed +- The `WordPress.NamingConventions.PrefixAllGlobals` sniff would underreport when global functions would be autoloaded via a Composer autoload `files` configuration. +- The `WordPress.Security.EscapeOutput` sniff will now recognize `map_deep()` for escaping the values in an array via a callback to an output escaping function. This should prevent false positives. +- The `WordPress.Security.NonceVerification` sniff will no longer inadvertently allow for a variable to be sanitized without a nonce check within the same scope. +- The `WordPress.Security.ValidatedSanitizedInput` sniff will no longer throw errors when a variable is only being type tested. +- The `WordPress.Security.ValidatedSanitizedInput` sniff will now correctly recognize the null coalesce (PHP 7.0) and null coalesce equal (PHP 7.4) operators and will now throw errors for missing unslashing and sanitization where relevant. +- The `WordPress.WP.AlternativeFunctions` sniff will no longer recommend using the WP_FileSystem when PHP native input streams, like `php://input`, or the PHP input stream constants are being read or written to. +- The `WordPress.WP.AlternativeFunctions` sniff will no longer report on usage of the `curl_version()` function. +- The `WordPress.WP.CronInterval` sniff now has improved function recognition which should lower the chance of false positives. +- The `WordPress.WP.EnqueuedResources` sniff will no longer throw false positives for inline jQuery code trying to access a stylesheet link tag. +- Various bugfixes for the `Sniff::has_nonce_check()` method: + - The method will no longer incorrectly identify methods/namespaced functions mirroring the name of WP native nonce verification functions as if they were the global functions. + This will prevent some false negatives. + - The method will now skip over nested closed scopes, such as closures and anonymous classes. This should prevent some false negatives for nonce verification being done while not in the correct scope. + + These fixes affect the `WordPress.Security.NonceVerification` sniff. +- The `Sniff::is_in_isset_or_empty()` method now also checks for usage of `array_key_exist()` and `key_exists()` and will regard these as correct ways to validate a variable. + This should prevent false positives for the `WordPress.Security.ValidatedSanitizedInput` and the `WordPress.Security.NonceVerification` sniffs. +- Various bugfixes for the `Sniff::is_sanitized()` method: + - The method presumed the WordPress coding style regarding code layout, which could lead to false positives. + - The method will no longer incorrectly identify methods/namespaced functions mirroring the name of WP/PHP native unslashing/sanitization functions as if they were the global functions. + This will prevent some false negatives. + - The method will now recognize `map_deep()` for sanitizing an array via a callback to a sanitization function. This should prevent false positives. + - The method will now recognize `stripslashes_deep()` and `stripslashes_from_strings_only()` as valid unslashing functions. This should prevent false positives. + All these fixes affect both the `WordPress.Security.ValidatedSanitizedInput` and the `WordPress.Security.NonceVerification` sniff. +- Various bugfixes for the `Sniff::is_validated()` method: + - The method did not verify correctly whether a variable being validated was the same variable as later used which could lead to false negatives. + - The method did not verify correctly whether a variable being validated had the same array index keys as the variable as later used which could lead to both false negatives as well as false positives. + - The method now also checks for usage of `array_key_exist()` and `key_exists()` and will regard these as correct ways to validate a variable. This should prevent some false positives. + - The methods will now recognize the null coalesce and the null coalesce equal operators as ways to validate a variable. This prevents some false positives. + The results from the `WordPress.Security.ValidatedSanitizedInput` sniff should be more accurate because of these fixes. +- A potential "Undefined index" notice from the `Sniff::is_assignment()` method. + + ## [2.0.0] - 2019-01-16 ### Important information about this release: @@ -994,6 +1070,7 @@ See the comparison for full list. Initial tagged release. [Unreleased]: https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/compare/master...HEAD +[2.1.0]: https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/compare/2.0.0...2.1.0 [2.0.0]: https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/compare/2.0.0-RC1...2.0.0 [2.0.0-RC1]: https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/compare/1.2.1...2.0.0-RC1 [1.2.1]: https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/compare/1.2.0...1.2.1 diff --git a/README.md b/README.md index b2662a15e1..17c4a52dc4 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ When installing the WordPress Coding Standards as a dependency in a larger proje There are two actively maintained Composer plugins which can handle the registration of standards with PHP_CodeSniffer for you: * [composer-phpcodesniffer-standards-plugin](https://github.com/higidi/composer-phpcodesniffer-standards-plugin) -* [phpcodesniffer-composer-installer](https://github.com/DealerDirect/phpcodesniffer-composer-installer):"^0.4.3" +* [phpcodesniffer-composer-installer](https://github.com/DealerDirect/phpcodesniffer-composer-installer):"^0.5.0" It is strongly suggested to `require` one of these plugins in your project to handle the registration of external standards with PHPCS for you. diff --git a/WordPress-Core/ruleset.xml b/WordPress-Core/ruleset.xml index 16edf427a6..9a3485b353 100644 --- a/WordPress-Core/ruleset.xml +++ b/WordPress-Core/ruleset.xml @@ -392,6 +392,18 @@ + + + + + + + diff --git a/WordPress-Docs/ruleset.xml b/WordPress-Docs/ruleset.xml index fb74597d76..e16001ed73 100644 --- a/WordPress-Docs/ruleset.xml +++ b/WordPress-Docs/ruleset.xml @@ -71,6 +71,8 @@ + + @@ -86,7 +88,7 @@ - + @@ -103,8 +105,5 @@ - - - diff --git a/WordPress-Extra/ruleset.xml b/WordPress-Extra/ruleset.xml index b03af3ee86..ffdb2310bf 100644 --- a/WordPress-Extra/ruleset.xml +++ b/WordPress-Extra/ruleset.xml @@ -24,12 +24,6 @@ - - - @@ -109,14 +103,9 @@ https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/issues/26 --> - - - - - + + diff --git a/WordPress/AbstractFunctionRestrictionsSniff.php b/WordPress/AbstractFunctionRestrictionsSniff.php index 5b11af4dad..4553e3f472 100644 --- a/WordPress/AbstractFunctionRestrictionsSniff.php +++ b/WordPress/AbstractFunctionRestrictionsSniff.php @@ -213,7 +213,15 @@ public function process_token( $stackPtr ) { public function is_targetted_token( $stackPtr ) { // Exclude function definitions, class methods, and namespaced calls. - if ( \T_STRING === $this->tokens[ $stackPtr ]['code'] && isset( $this->tokens[ ( $stackPtr - 1 ) ] ) ) { + if ( \T_STRING === $this->tokens[ $stackPtr ]['code'] ) { + if ( $this->is_class_object_call( $stackPtr ) === true ) { + return false; + } + + if ( $this->is_token_namespaced( $stackPtr ) === true ) { + return false; + } + $prev = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $stackPtr - 1 ), null, true ); if ( false !== $prev ) { @@ -222,21 +230,11 @@ public function is_targetted_token( $stackPtr ) { \T_FUNCTION => \T_FUNCTION, \T_CLASS => \T_CLASS, \T_AS => \T_AS, // Use declaration alias. - \T_DOUBLE_COLON => \T_DOUBLE_COLON, - \T_OBJECT_OPERATOR => \T_OBJECT_OPERATOR, ); if ( isset( $skipped[ $this->tokens[ $prev ]['code'] ] ) ) { return false; } - - // Skip namespaced functions, ie: \foo\bar() not \bar(). - if ( \T_NS_SEPARATOR === $this->tokens[ $prev ]['code'] ) { - $pprev = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $prev - 1 ), null, true ); - if ( false !== $pprev && \T_STRING === $this->tokens[ $pprev ]['code'] ) { - return false; - } - } } return true; diff --git a/WordPress/Sniff.php b/WordPress/Sniff.php index 721e85ecba..07d88fbe81 100644 --- a/WordPress/Sniff.php +++ b/WordPress/Sniff.php @@ -82,7 +82,7 @@ abstract class Sniff implements PHPCS_Sniff { * * @var string WordPress version. */ - public $minimum_supported_version = '4.7'; + public $minimum_supported_version = '4.8'; /** * Custom list of classes which test classes can extend. @@ -252,12 +252,10 @@ abstract class Sniff implements PHPCS_Sniff { */ protected $sanitizingFunctions = array( '_wp_handle_upload' => true, - 'array_key_exists' => true, 'esc_url_raw' => true, 'filter_input' => true, 'filter_var' => true, 'hash_equals' => true, - 'in_array' => true, 'is_email' => true, 'number_format' => true, 'sanitize_bookmark_field' => true, @@ -309,10 +307,57 @@ abstract class Sniff implements PHPCS_Sniff { protected $unslashingSanitizingFunctions = array( 'absint' => true, 'boolval' => true, + 'count' => true, + 'doubleval' => true, 'floatval' => true, 'intval' => true, - 'is_array' => true, 'sanitize_key' => true, + 'sizeof' => true, + ); + + /** + * Functions which unslash the data passed to them. + * + * @since 2.1.0 + * + * @var array + */ + protected $unslashingFunctions = array( + 'stripslashes_deep' => true, + 'stripslashes_from_strings_only' => true, + 'wp_unslash' => true, + ); + + /** + * List of PHP native functions to test the type of a variable. + * + * Using these functions is safe in combination with superglobals without + * unslashing or sanitization. + * + * They should, however, not be regarded as unslashing or sanitization functions. + * + * @since 2.1.0 + * + * @var array + */ + protected $typeTestFunctions = array( + 'is_array' => true, + 'is_bool' => true, + 'is_callable' => true, + 'is_countable' => true, + 'is_double' => true, + 'is_float' => true, + 'is_int' => true, + 'is_integer' => true, + 'is_iterable' => true, + 'is_long' => true, + 'is_null' => true, + 'is_numeric' => true, + 'is_object' => true, + 'is_real' => true, + 'is_resource' => true, + 'is_scalar' => true, + 'is_string' => true, ); /** @@ -328,6 +373,41 @@ abstract class Sniff implements PHPCS_Sniff { \T_BOOL_CAST => true, ); + /** + * List of array functions which apply a callback to the array. + * + * These are often used for sanitization/escaping an array variable. + * + * Note: functions which alter the array by reference are not listed here on purpose. + * These cannot easily be used for sanitization as they can't be combined with unslashing. + * Similarly, they cannot be used for late escaping as the return value is a boolean, not + * the altered array. + * + * @since 2.1.0 + * + * @var array => + */ + protected $arrayWalkingFunctions = array( + 'array_map' => 1, + 'map_deep' => 2, + ); + + /** + * Array functions to compare a $needle to a predefined set of values. + * + * If the value is set to an integer, the function needs to have at least that + * many parameters for it to be considered as a comparison. + * + * @since 2.1.0 + * + * @var array => + */ + protected $arrayCompareFunctions = array( + 'in_array' => true, + 'array_search' => true, + 'array_keys' => 2, + ); + /** * Functions that format strings. * @@ -770,6 +850,7 @@ abstract class Sniff implements PHPCS_Sniff { * @var string[] */ protected $test_class_whitelist = array( + 'WP_UnitTestCase_Base' => true, 'WP_UnitTestCase' => true, 'WP_Ajax_UnitTestCase' => true, 'WP_Canonical_UnitTestCase' => true, @@ -1320,7 +1401,9 @@ protected function is_assignment( $stackPtr ) { } // Check if this is an array assignment, e.g., `$var['key'] = 'val';` . - if ( \T_OPEN_SQUARE_BRACKET === $this->tokens[ $next_non_empty ]['code'] ) { + if ( \T_OPEN_SQUARE_BRACKET === $this->tokens[ $next_non_empty ]['code'] + && isset( $this->tokens[ $next_non_empty ]['bracket_closer'] ) + ) { return $this->is_assignment( $this->tokens[ $next_non_empty ]['bracket_closer'] ); } @@ -1367,12 +1450,21 @@ protected function has_nonce_check( $stackPtr ) { } } - $in_isset = $this->is_in_isset_or_empty( $stackPtr ); + $allow_nonce_after = false; + if ( $this->is_in_isset_or_empty( $stackPtr ) + || $this->is_in_type_test( $stackPtr ) + || $this->is_comparison( $stackPtr ) + || $this->is_in_array_comparison( $stackPtr ) + || $this->is_in_function_call( $stackPtr, $this->unslashingFunctions ) !== false + || $this->is_only_sanitized( $stackPtr ) + ) { + $allow_nonce_after = true; + } - // We allow for isset( $_POST['var'] ) checks to come before the nonce check. - // If this is inside an isset(), check after it as well, all the way to the - // end of the scope. - if ( $in_isset ) { + // We allow for certain actions, such as an isset() check to come before the nonce check. + // If this superglobal is inside such a check, look for the nonce after it as well, + // all the way to the end of the scope. + if ( true === $allow_nonce_after ) { $end = ( 0 === $start ) ? $this->phpcsFile->numTokens : $tokens[ $start ]['scope_closer']; } @@ -1388,7 +1480,7 @@ protected function has_nonce_check( $stackPtr ) { // If we have already found an nonce check in this scope, we just // need to check whether it comes before this token. It is OK if the // check is after the token though, if this was only a isset() check. - return ( $in_isset || $last['nonce_check'] < $stackPtr ); + return ( true === $allow_nonce_after || $last['nonce_check'] < $stackPtr ); } elseif ( $end <= $last['end'] ) { // If not, we can still go ahead and return false if we've already // checked to the end of the search area. @@ -1408,6 +1500,16 @@ protected function has_nonce_check( $stackPtr ) { // Loop through the tokens looking for nonce verification functions. for ( $i = $start; $i < $end; $i++ ) { + // Skip over nested closed scope constructs. + if ( \T_FUNCTION === $tokens[ $i ]['code'] + || \T_CLOSURE === $tokens[ $i ]['code'] + || isset( Tokens::$ooScopeTokens[ $tokens[ $i ]['code'] ] ) + ) { + if ( isset( $tokens[ $i ]['scope_closer'] ) ) { + $i = $tokens[ $i ]['scope_closer']; + } + continue; + } // If this isn't a function name, skip it. if ( \T_STRING !== $tokens[ $i ]['code'] ) { @@ -1416,6 +1518,17 @@ protected function has_nonce_check( $stackPtr ) { // If this is one of the nonce verification functions, we can bail out. if ( isset( $this->nonceVerificationFunctions[ $tokens[ $i ]['content'] ] ) ) { + /* + * Now, make sure it is a call to a global function. + */ + if ( $this->is_class_object_call( $i ) === true ) { + continue; + } + + if ( $this->is_token_namespaced( $i ) === true ) { + continue; + } + $last['nonce_check'] = $i; return true; } @@ -1428,9 +1541,11 @@ protected function has_nonce_check( $stackPtr ) { } /** - * Check if a token is inside of an isset() or empty() statement. + * Check if a token is inside of an isset(), empty() or array_key_exists() statement. * * @since 0.5.0 + * @since 2.1.0 Now checks for the token being used as the array parameter + * in function calls to array_key_exists() and key_exists() as well. * * @param int $stackPtr The index of the token in the stack. * @@ -1448,7 +1563,191 @@ protected function is_in_isset_or_empty( $stackPtr ) { $open_parenthesis = key( $nested_parenthesis ); $previous_non_empty = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $open_parenthesis - 1 ), null, true, null, true ); - return in_array( $this->tokens[ $previous_non_empty ]['code'], array( \T_ISSET, \T_EMPTY ), true ); + if ( false === $previous_non_empty ) { + return false; + } + + $previous_code = $this->tokens[ $previous_non_empty ]['code']; + if ( \T_ISSET === $previous_code || \T_EMPTY === $previous_code ) { + return true; + } + + $valid_functions = array( + 'array_key_exists' => true, + 'key_exists' => true, // Alias. + ); + + $functionPtr = $this->is_in_function_call( $stackPtr, $valid_functions ); + if ( false !== $functionPtr ) { + $second_param = $this->get_function_call_parameter( $functionPtr, 2 ); + if ( $stackPtr >= $second_param['start'] && $stackPtr <= $second_param['end'] ) { + return true; + } + } + + return false; + } + + /** + * Check if a particular token is a (static or non-static) call to a class method or property. + * + * @internal Note: this may still mistake a namespaced function imported via a `use` statement for + * a global function! + * + * @since 2.1.0 + * + * @param int $stackPtr The index of the token in the stack. + * + * @return bool + */ + protected function is_class_object_call( $stackPtr ) { + $before = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $stackPtr - 1 ), null, true, null, true ); + + if ( false === $before ) { + return false; + } + + if ( \T_OBJECT_OPERATOR !== $this->tokens[ $before ]['code'] + && \T_DOUBLE_COLON !== $this->tokens[ $before ]['code'] + ) { + return false; + } + + return true; + } + + /** + * Check if a particular token is prefixed with a namespace. + * + * @internal This will give a false positive if the file is not namespaced and the token is prefixed + * with `namespace\`. + * + * @since 2.1.0 + * + * @param int $stackPtr The index of the token in the stack. + * + * @return bool + */ + protected function is_token_namespaced( $stackPtr ) { + $prev = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $stackPtr - 1 ), null, true, null, true ); + + if ( false === $prev ) { + return false; + } + + if ( \T_NS_SEPARATOR !== $this->tokens[ $prev ]['code'] ) { + return false; + } + + $before_prev = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $prev - 1 ), null, true, null, true ); + if ( false === $before_prev ) { + return false; + } + + if ( \T_STRING !== $this->tokens[ $before_prev ]['code'] + && \T_NAMESPACE !== $this->tokens[ $before_prev ]['code'] + ) { + return false; + } + + return true; + } + + /** + * Check if a token is (part of) a parameter for a function call to a select list of functions. + * + * This is useful, for instance, when trying to determine the context a variable is used in. + * + * For example: this function could be used to determine if the variable `$foo` is used + * in a global function call to the function `is_foo()`. + * In that case, a call to this function would return the stackPtr to the T_STRING `is_foo` + * for code like: `is_foo( $foo, 'some_other_param' )`, while it would return `false` for + * the following code `is_bar( $foo, 'some_other_param' )`. + * + * @since 2.1.0 + * + * @param int $stackPtr The index of the token in the stack. + * @param array $valid_functions List of valid function names. + * Note: The keys to this array should be the function names + * in lowercase. Values are irrelevant. + * @param bool $global Optional. Whether to make sure that the function call is + * to a global function. If `false`, calls to methods, be it static + * `Class::method()` or via an object `$obj->method()`, and + * namespaced function calls, like `MyNS\function_name()` will + * also be accepted. + * Defaults to `true`. + * @param bool $allow_nested Optional. Whether to allow for nested function calls within the + * call to this function. + * I.e. when checking whether a token is within a function call + * to `strtolower()`, whether to accept `strtolower( trim( $var ) )` + * or only `strtolower( $var )`. + * Defaults to `false`. + * + * @return int|bool Stack pointer to the function call T_STRING token or false otherwise. + */ + protected function is_in_function_call( $stackPtr, $valid_functions, $global = true, $allow_nested = false ) { + if ( ! isset( $this->tokens[ $stackPtr ]['nested_parenthesis'] ) ) { + return false; + } + + $nested_parenthesis = $this->tokens[ $stackPtr ]['nested_parenthesis']; + if ( false === $allow_nested ) { + $nested_parenthesis = array_reverse( $nested_parenthesis, true ); + } + + foreach ( $nested_parenthesis as $open => $close ) { + + $prev_non_empty = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $open - 1 ), null, true, null, true ); + if ( false === $prev_non_empty || \T_STRING !== $this->tokens[ $prev_non_empty ]['code'] ) { + continue; + } + + if ( isset( $valid_functions[ strtolower( $this->tokens[ $prev_non_empty ]['content'] ) ] ) === false ) { + if ( false === $allow_nested ) { + // Function call encountered, but not to one of the allowed functions. + return false; + } + + continue; + } + + if ( false === $global ) { + return $prev_non_empty; + } + + /* + * Now, make sure it is a global function. + */ + if ( $this->is_class_object_call( $prev_non_empty ) === true ) { + continue; + } + + if ( $this->is_token_namespaced( $prev_non_empty ) === true ) { + continue; + } + + return $prev_non_empty; + } + + return false; + } + + /** + * Check if a token is inside of an is_...() statement. + * + * @since 2.1.0 + * + * @param int $stackPtr The index of the token in the stack. + * + * @return bool Whether the token is being type tested. + */ + protected function is_in_type_test( $stackPtr ) { + /* + * Casting the potential integer stack pointer return value to boolean here is fine. + * The return can never be `0` as there will always be a PHP open tag before the + * function call. + */ + return (bool) $this->is_in_function_call( $stackPtr, $this->typeTestFunctions ); } /** @@ -1517,8 +1816,8 @@ protected function is_safe_casted( $stackPtr ) { * @since 0.5.0 * * @param int $stackPtr The index of the token in the stack. - * @param bool $require_unslash Whether to give an error if wp_unslash() isn't - * used on the variable before sanitization. + * @param bool $require_unslash Whether to give an error if no unslashing function + * is used on the variable before sanitization. * * @return bool Whether the token being sanitized. */ @@ -1534,54 +1833,67 @@ protected function is_sanitized( $stackPtr, $require_unslash = false ) { if ( $require_unslash ) { $this->add_unslash_error( $stackPtr ); } + return false; } // Get the function that it's in. $nested_parenthesis = $this->tokens[ $stackPtr ]['nested_parenthesis']; - $function_closer = end( $nested_parenthesis ); - $function_opener = key( $nested_parenthesis ); - $function = $this->tokens[ ( $function_opener - 1 ) ]; + $nested_openers = array_keys( $nested_parenthesis ); + $function_opener = array_pop( $nested_openers ); + $functionPtr = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $function_opener - 1 ), null, true, null, true ); // If it is just being unset, the value isn't used at all, so it's safe. - if ( \T_UNSET === $function['code'] ) { + if ( \T_UNSET === $this->tokens[ $functionPtr ]['code'] ) { return true; } - // If this isn't a call to a function, it sure isn't sanitizing function. - if ( \T_STRING !== $function['code'] ) { - if ( $require_unslash ) { + $valid_functions = $this->sanitizingFunctions; + $valid_functions += $this->unslashingSanitizingFunctions; + $valid_functions += $this->unslashingFunctions; + $valid_functions += $this->arrayWalkingFunctions; + + $functionPtr = $this->is_in_function_call( $stackPtr, $valid_functions ); + + // If this isn't a call to one of the valid functions, it sure isn't a sanitizing function. + if ( false === $functionPtr ) { + if ( true === $require_unslash ) { $this->add_unslash_error( $stackPtr ); } + return false; } - $functionName = $function['content']; + $functionName = $this->tokens[ $functionPtr ]['content']; + + // Check if an unslashing function is being used. + if ( isset( $this->unslashingFunctions[ $functionName ] ) ) { - // Check if wp_unslash() is being used. - if ( 'wp_unslash' === $functionName ) { + $is_unslashed = true; - $is_unslashed = true; - $function_closer = prev( $nested_parenthesis ); + // Remove the unslashing functions. + $valid_functions = array_diff_key( $valid_functions, $this->unslashingFunctions ); - // If there is no other function being used, this value is unsanitized. - if ( ! $function_closer ) { + // Check is any of the remaining (sanitizing) functions is used. + $higherFunctionPtr = $this->is_in_function_call( $functionPtr, $valid_functions ); + + // If there is no other valid function being used, this value is unsanitized. + if ( false === $higherFunctionPtr ) { return false; } - $function_opener = key( $nested_parenthesis ); - $functionName = $this->tokens[ ( $function_opener - 1 ) ]['content']; + $functionPtr = $higherFunctionPtr; + $functionName = $this->tokens[ $functionPtr ]['content']; } else { - $is_unslashed = false; } - // Arrays might be sanitized via array_map(). - if ( 'array_map' === $functionName ) { + // Arrays might be sanitized via an array walking function using a callback. + if ( isset( $this->arrayWalkingFunctions[ $functionName ] ) ) { - // Get the first parameter. - $callback = $this->get_function_call_parameter( ( $function_opener - 1 ), 1 ); + // Get the callback parameter. + $callback = $this->get_function_call_parameter( $functionPtr, $this->arrayWalkingFunctions[ $functionName ] ); if ( ! empty( $callback ) ) { /* @@ -1615,7 +1927,7 @@ protected function is_sanitized( $stackPtr, $require_unslash = false ) { } /** - * Add an error for missing use of wp_unslash(). + * Add an error for missing use of unslashing. * * @since 0.5.0 * @@ -1624,19 +1936,73 @@ protected function is_sanitized( $stackPtr, $require_unslash = false ) { public function add_unslash_error( $stackPtr ) { $this->phpcsFile->addError( - 'Missing wp_unslash() before sanitization.', + '%s data not unslashed before sanitization. Use wp_unslash() or similar', $stackPtr, 'MissingUnslash', array( $this->tokens[ $stackPtr ]['content'] ) ); } + /** + * Get the index keys of an array variable. + * + * E.g., "bar" and "baz" in $foo['bar']['baz']. + * + * @since 2.1.0 + * + * @param int $stackPtr The index of the variable token in the stack. + * @param bool $all Whether to get all keys or only the first. + * Defaults to `true`(= all). + * + * @return array An array of index keys whose value is being accessed. + * or an empty array if this is not array access. + */ + protected function get_array_access_keys( $stackPtr, $all = true ) { + + $keys = array(); + + if ( \T_VARIABLE !== $this->tokens[ $stackPtr ]['code'] ) { + return $keys; + } + + $current = $stackPtr; + + do { + // Find the next non-empty token. + $open_bracket = $this->phpcsFile->findNext( + Tokens::$emptyTokens, + ( $current + 1 ), + null, + true + ); + + // If it isn't a bracket, this isn't an array-access. + if ( false === $open_bracket + || \T_OPEN_SQUARE_BRACKET !== $this->tokens[ $open_bracket ]['code'] + || ! isset( $this->tokens[ $open_bracket ]['bracket_closer'] ) + ) { + break; + } + + $key = $this->phpcsFile->getTokensAsString( + ( $open_bracket + 1 ), + ( $this->tokens[ $open_bracket ]['bracket_closer'] - $open_bracket - 1 ) + ); + + $keys[] = trim( $key ); + $current = $this->tokens[ $open_bracket ]['bracket_closer']; + } while ( isset( $this->tokens[ $current ] ) && true === $all ); + + return $keys; + } + /** * Get the index key of an array variable. * * E.g., "bar" in $foo['bar']. * * @since 0.5.0 + * @since 2.1.0 Now uses get_array_access_keys() under the hood. * * @param int $stackPtr The index of the token in the stack. * @@ -1644,29 +2010,18 @@ public function add_unslash_error( $stackPtr ) { */ protected function get_array_access_key( $stackPtr ) { - // Find the next non-empty token. - $open_bracket = $this->phpcsFile->findNext( - Tokens::$emptyTokens, - ( $stackPtr + 1 ), - null, - true - ); + $keys = $this->get_array_access_keys( $stackPtr, false ); - // If it isn't a bracket, this isn't an array-access. - if ( false === $open_bracket || \T_OPEN_SQUARE_BRACKET !== $this->tokens[ $open_bracket ]['code'] ) { - return false; + if ( isset( $keys[0] ) ) { + return $keys[0]; } - $key = $this->phpcsFile->getTokensAsString( - ( $open_bracket + 1 ), - ( $this->tokens[ $open_bracket ]['bracket_closer'] - $open_bracket - 1 ) - ); - - return trim( $key ); + return false; } /** - * Check if the existence of a variable is validated with isset() or empty(). + * 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: @@ -1689,17 +2044,21 @@ protected function get_array_access_key( $stackPtr ) { * ``` * * @since 0.5.0 - * - * @param int $stackPtr The index of this token in the stack. - * @param string $array_key An array key to check for ("bar" in $foo['bar']). - * @param bool $in_condition_only Whether to require that this use of the - * variable occur within the scope of the - * validating condition, or just in the same - * scope as it (default). + * @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. + * + * @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 occur within the scope of the + * validating condition, or just in the same + * scope as it (default). * * @return bool Whether the var is validated. */ - protected function is_validated( $stackPtr, $array_key = null, $in_condition_only = false ) { + protected function is_validated( $stackPtr, $array_keys = array(), $in_condition_only = false ) { if ( $in_condition_only ) { /* @@ -1750,36 +2109,168 @@ protected function is_validated( $stackPtr, $array_key = null, $in_condition_onl } $scope_end = $stackPtr; + } + if ( ! empty( $array_keys ) && ! is_array( $array_keys ) ) { + $array_keys = (array) $array_keys; } - $bare_array_key = $this->strip_quotes( $array_key ); + $bare_array_keys = array_map( array( $this, 'strip_quotes' ), $array_keys ); + $targets = array( + \T_ISSET => 'construct', + \T_EMPTY => 'construct', + \T_UNSET => 'construct', + \T_STRING => 'function_call', + \T_COALESCE => 'coalesce', + \T_COALESCE_EQUAL => 'coalesce', + ); // phpcs:ignore Generic.CodeAnalysis.JumbledIncrementer.Found -- On purpose, see below. for ( $i = ( $scope_start + 1 ); $i < $scope_end; $i++ ) { - if ( ! \in_array( $this->tokens[ $i ]['code'], array( \T_ISSET, \T_EMPTY, \T_UNSET ), true ) ) { + if ( isset( $targets[ $this->tokens[ $i ]['code'] ] ) === false ) { continue; } - $issetOpener = $this->phpcsFile->findNext( \T_OPEN_PARENTHESIS, $i ); - $issetCloser = $this->tokens[ $issetOpener ]['parenthesis_closer']; + switch ( $targets[ $this->tokens[ $i ]['code'] ] ) { + case 'construct': + $issetOpener = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $i + 1 ), null, true, null, true ); + if ( false === $issetOpener || \T_OPEN_PARENTHESIS !== $this->tokens[ $issetOpener ]['code'] ) { + // Parse error or live coding. + continue 2; + } - // Look for this variable. We purposely stomp $i from the parent loop. - for ( $i = ( $issetOpener + 1 ); $i < $issetCloser; $i++ ) { + $issetCloser = $this->tokens[ $issetOpener ]['parenthesis_closer']; - if ( \T_VARIABLE !== $this->tokens[ $i ]['code'] ) { - continue; - } + // Look for this variable. We purposely stomp $i from the parent loop. + for ( $i = ( $issetOpener + 1 ); $i < $issetCloser; $i++ ) { - // If we're checking for a specific array key (ex: 'hello' in - // $_POST['hello']), that must match too. Quote-style, however, doesn't matter. - if ( isset( $array_key ) - && $this->strip_quotes( $this->get_array_access_key( $i ) ) !== $bare_array_key ) { - continue; - } + if ( \T_VARIABLE !== $this->tokens[ $i ]['code'] ) { + continue; + } - return true; + if ( $this->tokens[ $stackPtr ]['content'] !== $this->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 = $this->get_array_access_keys( $i ); + $found_keys = array_map( array( $this, 'strip_quotes' ), $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 ( 'array_key_exists' !== $this->tokens[ $i ]['content'] + && 'key_exists' !== $this->tokens[ $i ]['content'] + ) { + continue 2; + } + + $next_non_empty = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $i + 1 ), null, true, null, true ); + if ( false === $next_non_empty || \T_OPEN_PARENTHESIS !== $this->tokens[ $next_non_empty ]['code'] ) { + // Not a function call. + continue 2; + } + + if ( $this->is_class_object_call( $i ) === true ) { + // Method call. + continue 2; + } + + if ( $this->is_token_namespaced( $i ) === true ) { + // Namespaced function call. + continue 2; + } + + $params = $this->get_function_call_parameters( $i ); + if ( count( $params ) < 2 ) { + continue 2; + } + + $param2_first_token = $this->phpcsFile->findNext( Tokens::$emptyTokens, $params[2]['start'], ( $params[2]['end'] + 1 ), true ); + if ( false === $param2_first_token + || \T_VARIABLE !== $this->tokens[ $param2_first_token ]['code'] + || $this->tokens[ $param2_first_token ]['content'] !== $this->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 first and the second parameter, but could also be completely in the second + * parameter, so we need to check both options. + */ + + $found_keys = $this->get_array_access_keys( $param2_first_token ); + $found_keys = array_map( array( $this, 'strip_quotes' ), $found_keys ); + + // First try matching the complete set against the second 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 + // second parameter and the last key against the first. + if ( $bare_keys === $found_keys && $this->strip_quotes( $params[1]['raw'] ) === $last_key ) { + return true; + } + + // Didn't find the correct array keys. + continue 2; + } + + return true; + + case 'coalesce': + $prev = $i; + do { + $prev = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $prev - 1 ), null, true, null, true ); + // Skip over array keys, like $_GET['key']['subkey']. + if ( \T_CLOSE_SQUARE_BRACKET === $this->tokens[ $prev ]['code'] ) { + $prev = $this->tokens[ $prev ]['bracket_opener']; + continue; + } + + break; + } while ( $prev >= ( $scope_start + 1 ) ); + + // We should now have reached the variable. + if ( \T_VARIABLE !== $this->tokens[ $prev ]['code'] ) { + continue 2; + } + + if ( $this->tokens[ $prev ]['content'] !== $this->tokens[ $stackPtr ]['content'] ) { + continue 2; + } + + if ( ! empty( $bare_array_keys ) ) { + $found_keys = $this->get_array_access_keys( $prev ); + $found_keys = array_map( array( $this, 'strip_quotes' ), $found_keys ); + $diff = array_diff_assoc( $bare_array_keys, $found_keys ); + if ( ! empty( $diff ) ) { + continue 2; + } + } + + // Right variable, correct key. + return true; } } @@ -1794,12 +2285,24 @@ protected function is_validated( $stackPtr, $array_key = null, $in_condition_onl * Also recognizes `switch ( $var )`. * * @since 0.5.0 + * @since 2.1.0 Added the $include_coalesce parameter. * - * @param int $stackPtr The index of this token in the stack. + * @param int $stackPtr The index of this token in the stack. + * @param bool $include_coalesce Optional. Whether or not to regard the null + * coalesce operator - ?? - as a comparison operator. + * Defaults to true. + * Null coalesce is a special comparison operator in this + * sense as it doesn't compare a variable to whatever is + * on the other side of the comparison operator. * * @return bool Whether this is a comparison. */ - protected function is_comparison( $stackPtr ) { + protected function is_comparison( $stackPtr, $include_coalesce = true ) { + + $comparisonTokens = Tokens::$comparisonTokens; + if ( false === $include_coalesce ) { + unset( $comparisonTokens[ \T_COALESCE ] ); + } // We first check if this is a switch statement (switch ( $var )). if ( isset( $this->tokens[ $stackPtr ]['nested_parenthesis'] ) ) { @@ -1823,7 +2326,7 @@ protected function is_comparison( $stackPtr ) { true ); - if ( isset( Tokens::$comparisonTokens[ $this->tokens[ $previous_token ]['code'] ] ) ) { + if ( isset( $comparisonTokens[ $this->tokens[ $previous_token ]['code'] ] ) ) { return true; } @@ -1836,7 +2339,7 @@ protected function is_comparison( $stackPtr ) { ); // This might be an opening square bracket in the case of arrays ($var['a']). - while ( \T_OPEN_SQUARE_BRACKET === $this->tokens[ $next_token ]['code'] ) { + while ( false !== $next_token && \T_OPEN_SQUARE_BRACKET === $this->tokens[ $next_token ]['code'] ) { $next_token = $this->phpcsFile->findNext( Tokens::$emptyTokens, @@ -1846,7 +2349,35 @@ protected function is_comparison( $stackPtr ) { ); } - if ( isset( Tokens::$comparisonTokens[ $this->tokens[ $next_token ]['code'] ] ) ) { + if ( false !== $next_token && isset( $comparisonTokens[ $this->tokens[ $next_token ]['code'] ] ) ) { + return true; + } + + return false; + } + + /** + * Check if a token is inside of an array-value comparison function. + * + * @since 2.1.0 + * + * @param int $stackPtr The index of the token in the stack. + * + * @return bool Whether the token is (part of) a parameter to an + * array-value comparison function. + */ + protected function is_in_array_comparison( $stackPtr ) { + $function_ptr = $this->is_in_function_call( $stackPtr, $this->arrayCompareFunctions, true, true ); + if ( false === $function_ptr ) { + return false; + } + + $function_name = $this->tokens[ $function_ptr ]['content']; + if ( true === $this->arrayCompareFunctions[ $function_name ] ) { + return true; + } + + if ( $this->get_function_call_parameter_count( $function_ptr ) >= $this->arrayCompareFunctions[ $function_name ] ) { return true; } @@ -2578,10 +3109,7 @@ public function is_use_of_global_constant( $stackPtr ) { return false; } - if ( false !== $prev - && \T_NS_SEPARATOR === $this->tokens[ $prev ]['code'] - && \T_STRING === $this->tokens[ ( $prev - 1 ) ]['code'] - ) { + if ( $this->is_token_namespaced( $stackPtr ) === true ) { // Namespaced constant of the same name. return false; } diff --git a/WordPress/Sniffs/NamingConventions/PrefixAllGlobalsSniff.php b/WordPress/Sniffs/NamingConventions/PrefixAllGlobalsSniff.php index 1dd90a3fb8..de46e0bdae 100644 --- a/WordPress/Sniffs/NamingConventions/PrefixAllGlobalsSniff.php +++ b/WordPress/Sniffs/NamingConventions/PrefixAllGlobalsSniff.php @@ -173,6 +173,18 @@ class PrefixAllGlobalsSniff extends AbstractFunctionParameterSniff { 'WP_DEFAULT_THEME' => true, ); + /** + * List of all PHP native functions. + * + * Using this list rather than a call to `function_exists()` prevents + * false negatives from user-defined functions when those would be + * autoloaded via a Composer autoload files directives. + * + * @var array + */ + private $built_in_functions; + + /** * Returns an array of tokens this test wants to listen for. * @@ -181,6 +193,11 @@ class PrefixAllGlobalsSniff extends AbstractFunctionParameterSniff { * @return array */ public function register() { + // Get a list of all PHP native functions. + $all_functions = get_defined_functions(); + $this->built_in_functions = array_flip( $all_functions['internal'] ); + + // Set the sniff targets. $targets = array( \T_NAMESPACE => \T_NAMESPACE, \T_FUNCTION => \T_FUNCTION, @@ -345,12 +362,12 @@ public function process_token( $stackPtr ) { } $item_name = $this->phpcsFile->getDeclarationName( $stackPtr ); - if ( function_exists( '\\' . $item_name ) ) { + if ( isset( $this->built_in_functions[ $item_name ] ) ) { // Backfill for PHP native function. return; } - $error_text = 'Functions declared'; + $error_text = 'Functions declared in the global namespace'; $error_code = 'NonPrefixedFunctionFound'; break; @@ -538,7 +555,7 @@ protected function process_variable_variable( $stackPtr ) { $stackPtr, 'NonPrefixedVariableFound', array( - 'Variables defined', + 'Global variables defined', $variable_name, ) ); @@ -688,7 +705,7 @@ protected function process_variable_assignment( $stackPtr ) { $is_error, 'NonPrefixedVariableFound', array( - 'Variables defined', + 'Global variables defined', '$' . $variable_name, ) ); diff --git a/WordPress/Sniffs/PHP/DiscouragedPHPFunctionsSniff.php b/WordPress/Sniffs/PHP/DiscouragedPHPFunctionsSniff.php index f7d613df83..e2691c2e60 100644 --- a/WordPress/Sniffs/PHP/DiscouragedPHPFunctionsSniff.php +++ b/WordPress/Sniffs/PHP/DiscouragedPHPFunctionsSniff.php @@ -56,12 +56,10 @@ public function getGroups() { 'runtime_configuration' => array( 'type' => 'warning', - 'message' => '%s() found. Changing configuration at runtime is rarely necessary.', + 'message' => '%s() found. Changing configuration values at runtime is strongly discouraged.', 'functions' => array( 'error_reporting', - 'ini_alter', 'ini_restore', - 'ini_set', 'apache_setenv', 'putenv', 'set_include_path', diff --git a/WordPress/Sniffs/PHP/IniSetSniff.php b/WordPress/Sniffs/PHP/IniSetSniff.php new file mode 100644 index 0000000000..afd58f1bf8 --- /dev/null +++ b/WordPress/Sniffs/PHP/IniSetSniff.php @@ -0,0 +1,177 @@ + true, + 'ini_alter' => true, + ); + + /** + * Array of PHP configuration options that are allowed to be manipulated. + * + * @since 2.1.0 + * + * @var array Multidimensional array with parameter details. + * $whitelisted_options = array( + * (string) option name. = array( + * (string[]) 'valid_values' = array() + * ) + * ); + */ + protected $whitelisted_options = array( + 'auto_detect_line_endings' => array(), + 'highlight.bg' => array(), + 'highlight.comment' => array(), + 'highlight.default' => array(), + 'highlight.html' => array(), + 'highlight.keyword' => array(), + 'highlight.string' => array(), + 'short_open_tag' => array( + 'valid_values' => array( 'true', '1', 'on' ), + ), + ); + + /** + * Array of PHP configuration options that are not allowed to be manipulated. + * + * @since 2.1.0 + * + * @var array Multidimensional array with parameter details. + * $blacklisted_options = array( + * (string) option name. = array( + * (string[]) 'invalid_values' = array() + * (string) 'message' + * ) + * ); + */ + protected $blacklisted_options = array( + 'bcmath.scale' => array( + 'message' => 'Use `bcscale()` instead.', + ), + 'display_errors' => array( + 'message' => 'Use `WP_DEBUG_DISPLAY` instead.', + ), + 'error_reporting' => array( + 'message' => 'Use `WP_DEBUG` instead.', + ), + 'filter.default' => array( + 'message' => 'Changing the option value can break other plugins. Use the filter flag constants when calling the Filter functions instead.', + ), + 'filter.default_flags' => array( + 'message' => 'Changing the option value can break other plugins. Use the filter flag constants when calling the Filter functions instead.', + ), + 'iconv.input_encoding' => array( + 'message' => 'PHP < 5.6 only - use `iconv_set_encoding()` instead.', + ), + 'iconv.internal_encoding' => array( + 'message' => 'PHP < 5.6 only - use `iconv_set_encoding()` instead.', + ), + 'iconv.output_encoding' => array( + 'message' => 'PHP < 5.6 only - use `iconv_set_encoding()` instead.', + ), + 'ignore_user_abort' => array( + 'message' => 'Use `ignore_user_abort()` instead.', + ), + 'log_errors' => array( + 'message' => 'Use `WP_DEBUG_LOG` instead.', + ), + 'max_execution_time' => array( + 'message' => 'Use `set_time_limit()` instead.', + ), + 'memory_limit' => array( + 'message' => 'Use `wp_raise_memory_limit()` or hook into the filters in that function.', + ), + 'short_open_tag' => array( + 'invalid_values' => array( 'false', '0', 'off' ), + 'message' => 'Turning off short_open_tag is prohibited as it can break other plugins.', + ), + ); + + /** + * Process the parameter of a matched function. + * + * Errors if an option is found in the blacklist. Warns as + * 'risky' when the option is not found in the whitelist. + * + * @since 2.1.0 + * + * @param int $stackPtr The position of the current token in the stack. + * @param array $group_name The name of the group which was matched. + * @param string $matched_content The token content (function name) which was matched. + * @param array $parameters Array with information about the parameters. + * + * @return void + */ + public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) { + $option_name = $this->strip_quotes( $parameters[1]['raw'] ); + $option_value = $this->strip_quotes( $parameters[2]['raw'] ); + if ( isset( $this->whitelisted_options[ $option_name ] ) ) { + $whitelisted_option = $this->whitelisted_options[ $option_name ]; + if ( ! isset( $whitelisted_option['valid_values'] ) || in_array( strtolower( $option_value ), $whitelisted_option['valid_values'], true ) ) { + return; + } + } + + if ( isset( $this->blacklisted_options[ $option_name ] ) ) { + $blacklisted_option = $this->blacklisted_options[ $option_name ]; + if ( ! isset( $blacklisted_option['invalid_values'] ) || in_array( strtolower( $option_value ), $blacklisted_option['invalid_values'], true ) ) { + $this->phpcsFile->addError( + '%s(%s, %s) found. %s', + $stackPtr, + $this->string_to_errorcode( $option_name . '_Blacklisted' ), + array( + $matched_content, + $parameters[1]['raw'], + $parameters[2]['raw'], + $blacklisted_option['message'], + ) + ); + return; + } + } + + $this->phpcsFile->addWarning( + '%s(%s, %s) found. Changing configuration values at runtime is strongly discouraged.', + $stackPtr, + 'Risky', + array( + $matched_content, + $parameters[1]['raw'], + $parameters[2]['raw'], + ) + ); + } +} diff --git a/WordPress/Sniffs/Security/EscapeOutputSniff.php b/WordPress/Sniffs/Security/EscapeOutputSniff.php index 9a1feaa857..d1dae3af85 100644 --- a/WordPress/Sniffs/Security/EscapeOutputSniff.php +++ b/WordPress/Sniffs/Security/EscapeOutputSniff.php @@ -382,20 +382,32 @@ public function process_token( $stackPtr ) { if ( false !== $function_opener ) { - if ( 'array_map' === $functionName ) { - - // Get the first parameter (name of function being used on the array). - $mapped_function = $this->phpcsFile->findNext( - Tokens::$emptyTokens, - ( $function_opener + 1 ), - $this->tokens[ $function_opener ]['parenthesis_closer'], - true + if ( isset( $this->arrayWalkingFunctions[ $functionName ] ) ) { + + // Get the callback parameter. + $callback = $this->get_function_call_parameter( + $ptr, + $this->arrayWalkingFunctions[ $functionName ] ); - // If we're able to resolve the function name, do so. - if ( $mapped_function && \T_CONSTANT_ENCAPSED_STRING === $this->tokens[ $mapped_function ]['code'] ) { - $functionName = $this->strip_quotes( $this->tokens[ $mapped_function ]['content'] ); - $ptr = $mapped_function; + 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 = $this->strip_quotes( $this->tokens[ $mapped_function ]['content'] ); + $ptr = $mapped_function; + } } } diff --git a/WordPress/Sniffs/Security/NonceVerificationSniff.php b/WordPress/Sniffs/Security/NonceVerificationSniff.php index b29dcb1fa0..e9b2cf6603 100644 --- a/WordPress/Sniffs/Security/NonceVerificationSniff.php +++ b/WordPress/Sniffs/Security/NonceVerificationSniff.php @@ -121,10 +121,6 @@ public function process_token( $stackPtr ) { $this->mergeFunctionLists(); - if ( $this->is_only_sanitized( $stackPtr ) ) { - return; - } - if ( $this->has_nonce_check( $stackPtr ) ) { return; } diff --git a/WordPress/Sniffs/Security/ValidatedSanitizedInputSniff.php b/WordPress/Sniffs/Security/ValidatedSanitizedInputSniff.php index 5681de7cea..4b65ac65be 100644 --- a/WordPress/Sniffs/Security/ValidatedSanitizedInputSniff.php +++ b/WordPress/Sniffs/Security/ValidatedSanitizedInputSniff.php @@ -10,6 +10,7 @@ namespace WordPressCS\WordPress\Sniffs\Security; use WordPressCS\WordPress\Sniff; +use PHP_CodeSniffer\Util\Tokens; /** * Flag any non-validated/sanitized input ( _GET / _POST / etc. ). @@ -123,26 +124,69 @@ function ( $symbol ) { return; } - $array_key = $this->get_array_access_key( $stackPtr ); + $array_keys = $this->get_array_access_keys( $stackPtr ); - if ( empty( $array_key ) ) { + if ( empty( $array_keys ) ) { return; } - $error_data = array( $this->tokens[ $stackPtr ]['content'] ); + $error_data = array( $this->tokens[ $stackPtr ]['content'] . '[' . implode( '][', $array_keys ) . ']' ); - // Check for validation first. - if ( ! $this->is_validated( $stackPtr, $array_key, $this->check_validation_in_scope_only ) ) { - $this->phpcsFile->addError( 'Detected usage of a non-validated input variable: %s', $stackPtr, 'InputNotValidated', $error_data ); - // return; // Should we just return and not look for sanitizing functions ? + /* + * Check for validation first. + */ + $validated = false; + + for ( $i = ( $stackPtr + 1 ); $i < $this->phpcsFile->numTokens; $i++ ) { + if ( isset( Tokens::$emptyTokens[ $this->tokens[ $i ]['code'] ] ) ) { + continue; + } + + if ( \T_OPEN_SQUARE_BRACKET === $this->tokens[ $i ]['code'] + && isset( $this->tokens[ $i ]['bracket_closer'] ) + ) { + // Skip over array keys. + $i = $this->tokens[ $i ]['bracket_closer']; + continue; + } + + if ( \T_COALESCE === $this->tokens[ $i ]['code'] ) { + $validated = true; + } + + // Anything else means this is not a validation coalesce. + break; + } + + if ( false === $validated ) { + $validated = $this->is_validated( $stackPtr, $array_keys, $this->check_validation_in_scope_only ); + } + + if ( false === $validated ) { + $this->phpcsFile->addError( + 'Detected usage of a possibly undefined superglobal array index: %s. Use isset() or empty() to check the index exists before using it', + $stackPtr, + 'InputNotValidated', + $error_data + ); } if ( $this->has_whitelist_comment( 'sanitization', $stackPtr ) ) { return; } + // If this variable is being tested with one of the `is_..()` functions, sanitization isn't needed. + if ( $this->is_in_type_test( $stackPtr ) ) { + return; + } + // If this is a comparison ('a' == $_POST['foo']), sanitization isn't needed. - if ( $this->is_comparison( $stackPtr ) ) { + if ( $this->is_comparison( $stackPtr, false ) ) { + return; + } + + // If this is a comparison using the array comparison functions, sanitization isn't needed. + if ( $this->is_in_array_comparison( $stackPtr ) ) { return; } @@ -150,7 +194,12 @@ function ( $symbol ) { // Now look for sanitizing functions. if ( ! $this->is_sanitized( $stackPtr, true ) ) { - $this->phpcsFile->addError( 'Detected usage of a non-sanitized input variable: %s', $stackPtr, 'InputNotSanitized', $error_data ); + $this->phpcsFile->addError( + 'Detected usage of a non-sanitized input variable: %s', + $stackPtr, + 'InputNotSanitized', + $error_data + ); } } diff --git a/WordPress/Sniffs/WP/AlternativeFunctionsSniff.php b/WordPress/Sniffs/WP/AlternativeFunctionsSniff.php index 68092de5d0..52e1356f21 100644 --- a/WordPress/Sniffs/WP/AlternativeFunctionsSniff.php +++ b/WordPress/Sniffs/WP/AlternativeFunctionsSniff.php @@ -25,6 +25,47 @@ */ class AlternativeFunctionsSniff extends AbstractFunctionRestrictionsSniff { + /** + * Local input streams which should not be flagged for the file system function checks. + * + * @link http://php.net/manual/en/wrappers.php.php + * + * @var array + */ + protected $allowed_local_streams = array( + 'php://input' => true, + 'php://output' => true, + 'php://stdin' => true, + 'php://stdout' => true, + 'php://stderr' => true, + ); + + /** + * Local input streams which should not be flagged for the file system function checks if + * the $filename starts with them. + * + * @link http://php.net/manual/en/wrappers.php.php + * + * @var array + */ + protected $allowed_local_stream_partials = array( + 'php://temp/', + 'php://fd/', + ); + + /** + * Local input stream constants which should not be flagged for the file system function checks. + * + * @link http://php.net/manual/en/wrappers.php.php + * + * @var array + */ + protected $allowed_local_stream_constants = array( + 'STDIN' => true, + 'STDOUT' => true, + 'STDERR' => true, + ); + /** * Groups of functions to restrict. * @@ -83,13 +124,13 @@ public function getGroups() { 'since' => '2.5.0', 'functions' => array( 'readfile', - 'fopen', - 'fsockopen', - 'pfsockopen', 'fclose', + 'fopen', 'fread', 'fwrite', 'file_put_contents', + 'fsockopen', + 'pfsockopen', ), ), @@ -202,9 +243,40 @@ public function process_matched_token( $stackPtr, $group_name, $matched_content return; } + if ( $this->is_local_data_stream( $params[1]['raw'] ) === true ) { + // Local data stream. + return; + } + unset( $params ); break; + + case 'readfile': + case 'fopen': + case 'file_put_contents': + /* + * Allow for handling raw data streams from the request body. + */ + $first_param = $this->get_function_call_parameter( $stackPtr, 1 ); + + if ( false === $first_param ) { + // If the file to work with is not set, local data streams don't come into play. + break; + } + + if ( $this->is_local_data_stream( $first_param['raw'] ) === true ) { + // Local data stream. + return; + } + + unset( $first_param ); + + break; + + case 'curl_version': + // Curl version doesn't actually create a connection. + return; } if ( ! isset( $this->groups[ $group_name ]['since'] ) ) { @@ -217,4 +289,29 @@ public function process_matched_token( $stackPtr, $group_name, $matched_content } } + /** + * Determine based on the "raw" parameter value, whether a file parameter points to + * a local data stream. + * + * @param string $raw_param_value Raw parameter value. + * + * @return bool True if this is a local data stream. False otherwise. + */ + protected function is_local_data_stream( $raw_param_value ) { + + $raw_stripped = $this->strip_quotes( $raw_param_value ); + if ( isset( $this->allowed_local_streams[ $raw_stripped ] ) + || isset( $this->allowed_local_stream_constants[ $raw_param_value ] ) + ) { + return true; + } + + foreach ( $this->allowed_local_stream_partials as $partial ) { + if ( strpos( $raw_stripped, $partial ) === 0 ) { + return true; + } + } + + return false; + } } diff --git a/WordPress/Sniffs/WP/CronIntervalSniff.php b/WordPress/Sniffs/WP/CronIntervalSniff.php index d7c81c2bb4..d7e91b8321 100644 --- a/WordPress/Sniffs/WP/CronIntervalSniff.php +++ b/WordPress/Sniffs/WP/CronIntervalSniff.php @@ -55,6 +55,15 @@ class CronIntervalSniff extends Sniff { 'YEAR_IN_SECONDS' => 31536000, ); + /** + * Function within which the hook should be found. + * + * @var array + */ + protected $valid_functions = array( + 'add_filter' => true, + ); + /** * Returns an array of tokens this test wants to listen for. * @@ -82,8 +91,8 @@ public function process_token( $stackPtr ) { } // If within add_filter. - $functionPtr = $this->phpcsFile->findPrevious( \T_STRING, key( $token['nested_parenthesis'] ) ); - if ( false === $functionPtr || 'add_filter' !== $this->tokens[ $functionPtr ]['content'] ) { + $functionPtr = $this->is_in_function_call( $stackPtr, $this->valid_functions ); + if ( false === $functionPtr ) { return; } @@ -92,6 +101,11 @@ public function process_token( $stackPtr ) { return; } + if ( $stackPtr >= $callback['start'] ) { + // "cron_schedules" found in the second parameter, not the first. + return; + } + // Detect callback function name. $callbackArrayPtr = $this->phpcsFile->findNext( Tokens::$emptyTokens, $callback['start'], ( $callback['end'] + 1 ), true ); diff --git a/WordPress/Sniffs/WP/DeprecatedClassesSniff.php b/WordPress/Sniffs/WP/DeprecatedClassesSniff.php index 69894fb644..cd69ba988b 100644 --- a/WordPress/Sniffs/WP/DeprecatedClassesSniff.php +++ b/WordPress/Sniffs/WP/DeprecatedClassesSniff.php @@ -58,13 +58,11 @@ class DeprecatedClassesSniff extends AbstractClassRestrictionsSniff { */ public function getGroups() { // Make sure all array keys are lowercase. - $keys = array_keys( $this->deprecated_classes ); - $keys = array_map( 'strtolower', $keys ); - $this->deprecated_classes = array_combine( $keys, $this->deprecated_classes ); + $this->deprecated_classes = array_change_key_case( $this->deprecated_classes, CASE_LOWER ); return array( 'deprecated_classes' => array( - 'classes' => $keys, + 'classes' => array_keys( $this->deprecated_classes ), ), ); } diff --git a/WordPress/Sniffs/WP/DeprecatedFunctionsSniff.php b/WordPress/Sniffs/WP/DeprecatedFunctionsSniff.php index 7270899004..8525dc33bb 100644 --- a/WordPress/Sniffs/WP/DeprecatedFunctionsSniff.php +++ b/WordPress/Sniffs/WP/DeprecatedFunctionsSniff.php @@ -579,6 +579,11 @@ class DeprecatedFunctionsSniff extends AbstractFunctionRestrictionsSniff { 'alt' => 'wp_die()', 'version' => '3.0.0', ), + // Verified version & alternative. + 'install_blog_defaults' => array( + 'alt' => 'wp_install_defaults', + 'version' => '3.0.0', + ), 'is_main_blog' => array( 'alt' => 'is_main_site()', 'version' => '3.0.0', @@ -1330,6 +1335,16 @@ class DeprecatedFunctionsSniff extends AbstractFunctionRestrictionsSniff { 'alt' => '', 'version' => '4.9.0', ), + + // WP 5.1.0. + 'insert_blog' => array( + 'alt' => 'wp_insert_site()', + 'version' => '5.1.0', + ), + 'install_blog' => array( + 'alt' => '', + 'version' => '5.1.0', + ), ); /** @@ -1339,13 +1354,11 @@ class DeprecatedFunctionsSniff extends AbstractFunctionRestrictionsSniff { */ public function getGroups() { // Make sure all array keys are lowercase. - $keys = array_keys( $this->deprecated_functions ); - $keys = array_map( 'strtolower', $keys ); - $this->deprecated_functions = array_combine( $keys, $this->deprecated_functions ); + $this->deprecated_functions = array_change_key_case( $this->deprecated_functions, CASE_LOWER ); return array( 'deprecated_functions' => array( - 'functions' => $keys, + 'functions' => array_keys( $this->deprecated_functions ), ), ); } diff --git a/WordPress/Sniffs/WP/DiscouragedConstantsSniff.php b/WordPress/Sniffs/WP/DiscouragedConstantsSniff.php index 00e1e8efa8..6539b49cc5 100644 --- a/WordPress/Sniffs/WP/DiscouragedConstantsSniff.php +++ b/WordPress/Sniffs/WP/DiscouragedConstantsSniff.php @@ -124,10 +124,7 @@ public function process_arbitrary_tstring( $stackPtr ) { return; } - if ( false !== $prev - && \T_NS_SEPARATOR === $this->tokens[ $prev ]['code'] - && \T_STRING === $this->tokens[ ( $prev - 1 ) ]['code'] - ) { + if ( $this->is_token_namespaced( $stackPtr ) === true ) { // Namespaced constant of the same name. return; } diff --git a/WordPress/Sniffs/WP/EnqueuedResourcesSniff.php b/WordPress/Sniffs/WP/EnqueuedResourcesSniff.php index 1473eac208..e8d0333ad4 100644 --- a/WordPress/Sniffs/WP/EnqueuedResourcesSniff.php +++ b/WordPress/Sniffs/WP/EnqueuedResourcesSniff.php @@ -44,7 +44,7 @@ public function register() { public function process_token( $stackPtr ) { $token = $this->tokens[ $stackPtr ]; - if ( preg_match( '#rel=\\\\?[\'"]?stylesheet\\\\?[\'"]?#', $token['content'] ) > 0 ) { + if ( preg_match( '# rel=\\\\?[\'"]?stylesheet\\\\?[\'"]?#', $token['content'] ) > 0 ) { $this->phpcsFile->addError( 'Stylesheets must be registered/enqueued via wp_enqueue_style', $stackPtr, diff --git a/WordPress/Sniffs/WP/GlobalVariablesOverrideSniff.php b/WordPress/Sniffs/WP/GlobalVariablesOverrideSniff.php index 4485c027ad..57b64d07a5 100644 --- a/WordPress/Sniffs/WP/GlobalVariablesOverrideSniff.php +++ b/WordPress/Sniffs/WP/GlobalVariablesOverrideSniff.php @@ -310,8 +310,7 @@ protected function process_global_statement( $stackPtr, $in_function_scope ) { } // Don't throw false positives for static class properties. - $previous = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $ptr - 1 ), null, true, null, true ); - if ( false !== $previous && \T_DOUBLE_COLON === $this->tokens[ $previous ]['code'] ) { + if ( $this->is_class_object_call( $ptr ) === true ) { continue; } @@ -321,17 +320,8 @@ protected function process_global_statement( $stackPtr, $in_function_scope ) { } // Check if this is a variable assignment within a `foreach()` declaration. - if ( isset( $this->tokens[ $ptr ]['nested_parenthesis'] ) ) { - $nested_parenthesis = $this->tokens[ $ptr ]['nested_parenthesis']; - $close_parenthesis = end( $nested_parenthesis ); - if ( isset( $this->tokens[ $close_parenthesis ]['parenthesis_owner'] ) - && \T_FOREACH === $this->tokens[ $this->tokens[ $close_parenthesis ]['parenthesis_owner'] ]['code'] - && ( false !== $previous - && ( \T_DOUBLE_ARROW === $this->tokens[ $previous ]['code'] - || \T_AS === $this->tokens[ $previous ]['code'] ) ) - ) { - $this->maybe_add_error( $ptr ); - } + if ( $this->is_foreach_as( $ptr ) === true ) { + $this->maybe_add_error( $ptr ); } } } diff --git a/WordPress/Tests/PHP/DevelopmentFunctionsUnitTest.inc b/WordPress/Tests/PHP/DevelopmentFunctionsUnitTest.inc index 5039be590e..b07e7a8ce6 100644 --- a/WordPress/Tests/PHP/DevelopmentFunctionsUnitTest.inc +++ b/WordPress/Tests/PHP/DevelopmentFunctionsUnitTest.inc @@ -32,3 +32,8 @@ phpinfo(); // Ok - within excluded group. // phpcs:set WordPress.PHP.DevelopmentFunctions exclude[] trigger_error(); // Error. phpinfo(); // Error. + +Wrapper_Class::var_dump(); // OK, not the native PHP function. +$wrapper ->var_dump(); // OK, not the native PHP function. +namespace\var_dump(); // OK as long as the file is namespaced. +MyNamespace\var_dump(); // OK, namespaced function. diff --git a/WordPress/Tests/PHP/DiscouragedPHPFunctionsUnitTest.inc b/WordPress/Tests/PHP/DiscouragedPHPFunctionsUnitTest.inc index 792b0cad31..9a17c8a6d8 100644 --- a/WordPress/Tests/PHP/DiscouragedPHPFunctionsUnitTest.inc +++ b/WordPress/Tests/PHP/DiscouragedPHPFunctionsUnitTest.inc @@ -14,9 +14,8 @@ rawurlencode(); // Ok. dl(); // Warning. error_reporting(); // Warning. -ini_alter(); // Warning. + ini_restore(); // Warning. -ini_set(); // Warning. magic_quotes_runtime(); // Warning. set_magic_quotes_runtime(); // Warning. apache_setenv(); // Warning. diff --git a/WordPress/Tests/PHP/DiscouragedPHPFunctionsUnitTest.php b/WordPress/Tests/PHP/DiscouragedPHPFunctionsUnitTest.php index db73fe44bd..0b43deffe5 100644 --- a/WordPress/Tests/PHP/DiscouragedPHPFunctionsUnitTest.php +++ b/WordPress/Tests/PHP/DiscouragedPHPFunctionsUnitTest.php @@ -42,7 +42,6 @@ public function getWarningList() { 12 => 1, 15 => 1, 16 => 1, - 17 => 1, 18 => 1, 19 => 1, 20 => 1, @@ -50,18 +49,17 @@ public function getWarningList() { 22 => 1, 23 => 1, 24 => 1, - 25 => 1, + 27 => 1, 28 => 1, 29 => 1, 30 => 1, 31 => 1, - 32 => 1, + 34 => 1, 35 => 1, 36 => 1, 37 => 1, 38 => 1, 39 => 1, - 40 => 1, ); } diff --git a/WordPress/Tests/PHP/IniSetUnitTest.inc b/WordPress/Tests/PHP/IniSetUnitTest.inc new file mode 100644 index 0000000000..b39d3e1a3b --- /dev/null +++ b/WordPress/Tests/PHP/IniSetUnitTest.inc @@ -0,0 +1,43 @@ + => + */ + public function getErrorList() { + return array( + 16 => 1, + 17 => 1, + 18 => 1, + 19 => 1, + 20 => 1, + 21 => 1, + 22 => 1, + 23 => 1, + 24 => 1, + 25 => 1, + 26 => 1, + 27 => 1, + 28 => 1, + 29 => 1, + 30 => 1, + 31 => 1, + 32 => 1, + 33 => 1, + 34 => 1, + 42 => 1, + ); + } + + /** + * Returns the lines where warnings should occur. + * + * @return array => + */ + public function getWarningList() { + return array( + 36 => 1, + 37 => 1, + 38 => 1, + 39 => 1, + 43 => 1, + ); + } + +} diff --git a/WordPress/Tests/Security/EscapeOutputUnitTest.inc b/WordPress/Tests/Security/EscapeOutputUnitTest.inc index bd7adb3930..875f575f23 100644 --- a/WordPress/Tests/Security/EscapeOutputUnitTest.inc +++ b/WordPress/Tests/Security/EscapeOutputUnitTest.inc @@ -289,3 +289,6 @@ echo esc_html( $something ), echo get_the_title(); // Bad. echo wp_kses_post( get_the_title() ); // Ok. echo esc_html( get_the_title() ); // Ok. + +echo implode( '
', map_deep( $items, 'esc_html' ) ); // Ok. +echo implode( '
', map_deep( $items, 'foo' ) ); // Bad. diff --git a/WordPress/Tests/Security/EscapeOutputUnitTest.php b/WordPress/Tests/Security/EscapeOutputUnitTest.php index 6fc0837e54..52b554be55 100644 --- a/WordPress/Tests/Security/EscapeOutputUnitTest.php +++ b/WordPress/Tests/Security/EscapeOutputUnitTest.php @@ -79,6 +79,7 @@ public function getErrorList() { 264 => 1, 266 => 1, 289 => 1, + 294 => 1, ); } diff --git a/WordPress/Tests/Security/NonceVerificationUnitTest.inc b/WordPress/Tests/Security/NonceVerificationUnitTest.inc index b5387355de..d02e6a7cb0 100644 --- a/WordPress/Tests/Security/NonceVerificationUnitTest.inc +++ b/WordPress/Tests/Security/NonceVerificationUnitTest.inc @@ -17,7 +17,7 @@ function ajax_process() { } add_action( 'wp_ajax_process', 'ajax_process' ); -// It's also OK to check with isset() before the the nonce check. +// It's also OK to check with isset() before the nonce check. function foo() { if ( ! isset( $_POST['test'] ) || ! wp_verify_nonce( 'some_action' ) ) { exit; @@ -160,3 +160,118 @@ function foo_8() { sanitize_pc( $_POST['bar'] ); // Bad. my_nonce_check( sanitize_twitter( $_POST['tweet'] ) ); // Bad. } + +/* + * Using a superglobal in a is_...() function is OK as long as a nonce check is done + * before the variable is *really* used. + */ +function test_whitelisting_use_in_type_test_functions() { + if ( ! is_numeric ( $_POST['foo'] ) ) { // OK. + return; + } + + wp_verify_nonce( 'some_action' ); +} + +function test_incorrect_use_in_type_test_functions() { + if ( ! is_numeric ( $_POST['foo'] ) ) { // Bad. + return; + } +} + +function fix_false_negatives_userland_method_same_name() { + WP_Faker::check_ajax_referer( 'something' ); + $faker->check_admin_referer( 'something' ); + do_something( $_POST['abc'] ); // Bad. +} + +function fix_false_negatives_namespaced_function_same_name() { + WP_Faker\SecurityBypass\wp_verify_nonce( 'something' ); + do_something( $_POST['abc'] ); // Bad. +} + +function skip_over_nested_constructs_1() { + $b = function () { + check_ajax_referer( 'something' ); // Nonce check is not in the same function scope. + }; + + do_something( $_POST['abc'] ); // Bad. +} + +function skip_over_nested_constructs_2() { + if ( $_POST['abc'] === 'test' ) { // Bad. + return; + } + + $b = new class() { + public function named() { + check_ajax_referer( 'something' ); // Nonce check is not in the same function scope. + } + }; +} + +// Issue #1506 +function allow_for_compare_before_noncecheck() { + if ( + 'newsletter_sign_up' === $_POST['action'] && // OK. + wp_verify_nonce( $_POST['newsletter_nonce'] ) + ) {} +} + +// Issue #1114 +function allow_for_nonce_check_within_switch() { + if ( ! isset( $_REQUEST['action'] ) ) { + return; + } + + switch ( $_REQUEST['action'] ) { // OK. + case 'foo': + check_admin_referer( 'foo' ); + break; + case 'bar': + check_admin_referer( 'bar' ); + break; + } +} + +function allow_for_array_compare_before_noncecheck() { + if ( array_search( array( 'subscribe', 'unsubscribe', $_POST['action'], true ) // OK. + && wp_verify_nonce( $_POST['newsletter_nonce'] ) + ) {} +} + +function allow_for_array_comparison_in_condition() { + if ( in_array( $_GET['action'], $valid_actions, true ) ) { // OK. + check_admin_referer( 'foo' ); + foo(); + } +} + +# Issue #572. +function allow_for_unslash_before_noncecheck_but_demand_noncecheck() { + $var = wp_unslash( $_POST['foo'] ); // Bad. + echo $var; +} + +function allow_for_unslash_before_noncecheck() { + $var = stripslashes_from_strings_only( $_POST['foo'] ); // OK. + wp_verify_nonce( $var ); + echo $var; +} + +function allow_for_unslash_in_sanitization() { + $var = sanitize_text_field( wp_unslash( $_POST['foo'] ) ); // OK. + wp_verify_nonce( $var ); + echo $var; +} + +function dont_allow_bypass_nonce_via_sanitization() { + $var = sanitize_text_field( $_POST['foo'] ); // Bad. + echo $var; +} + +function dont_allow_bypass_nonce_via_sanitization() { + $var = sanitize_text_field( $_POST['foo'] ); // OK. + wp_verify_nonce( $var ); + echo $var; +} diff --git a/WordPress/Tests/Security/NonceVerificationUnitTest.php b/WordPress/Tests/Security/NonceVerificationUnitTest.php index d937edb1e9..964ab9501a 100644 --- a/WordPress/Tests/Security/NonceVerificationUnitTest.php +++ b/WordPress/Tests/Security/NonceVerificationUnitTest.php @@ -46,6 +46,13 @@ public function getErrorList() { 159 => 1, 160 => 1, 161 => 1, + 177 => 1, + 185 => 1, + 190 => 1, + 198 => 1, + 202 => 1, + 252 => 1, + 269 => 1, ); } diff --git a/WordPress/Tests/Security/ValidatedSanitizedInputUnitTest.inc b/WordPress/Tests/Security/ValidatedSanitizedInputUnitTest.inc index 9a8e6c4844..46dad6254c 100644 --- a/WordPress/Tests/Security/ValidatedSanitizedInputUnitTest.inc +++ b/WordPress/Tests/Security/ValidatedSanitizedInputUnitTest.inc @@ -85,7 +85,7 @@ echo array_map( array( $obj, 'sanitize_text_field' ), wp_unslash( $_GET['test'] $foo = (int) $_POST['foo6']; // Bad. // Non-assignment checks are OK. -if ( 'bar' === $_POST['foo'] ) {} // Ok. +if ( isset( $_POST['foo'] ) && 'bar' === $_POST['foo'] ) {} // Ok. if ( $_GET['test'] != 'a' ) {} // Ok. if ( 'bar' === do_something( wp_unslash( $_POST['foo'] ) ) ) {} // Bad. @@ -161,7 +161,7 @@ some string {$_POST[some_var]} {$_GET['evil']} EOD ); // Bad x2. -if ( ( $_POST['foo'] ?? 'post' ) === 'post' ) {} // OK. +if ( ( $_POST['foo'] ?? 'post' ) === 'post' ) {} // Bad x2 - unslash, sanitize - more complex compares are not handled. if ( ( $_POST['foo'] <=> 'post' ) === 0 ) {} // OK. // Test whitespace independent isset/empty detection. @@ -183,3 +183,151 @@ function barfoo() { if ( isset( $_POST[ 'currentid' ] ) ){ $id = (int) $_POST[ "currentid" ]; // OK. } + +// Only recognize validation if the correct superglobal is examined. +if ( isset ( $_POST['thisisnotget'] ) ) { + $get = (int) $_GET['thisisnotget']; // Bad. +} + +// Recognize PHP native array_key_exists() as validation function. +if ( array_key_exists( 'my_field1', $_POST ) ) { + $id = (int) $_POST['my_field1']; // OK. +} + +if ( \array_key_exists( 'my_field2', $_POST ) ) { + $id = (int) $_POST['my_field2']; // OK. +} + +if ( \Some\ClassName\array_key_exists( 'my_field3', $_POST ) ) { + $id = (int) $_POST['my_field3']; // Bad. +} + +if ( $obj->array_key_exists( 'my_field4', $_POST ) ) { + $id = (int) $_POST['my_field4']; // Bad. +} + +if ( ClassName::array_key_exists( 'my_field5', $_POST ) ) { + $id = (int) $_POST['my_field5']; // Bad. +} + +echo sanitize_text_field (wp_unslash ($_GET['test'])); // OK. + +if ( isset( $_GET['unslash_check'] ) ) { + $clean = sanitize_text_field( WP_Faker::wp_unslash( $_GET['unslash_check'] ) ); // Bad x1 - unslash. + $clean = WP_Faker\sanitize_text_field( wp_unslash( $_GET['unslash_check'] ) ); // Bad x1 - sanitize. +} + +function test_more_safe_functions() { + if ( ! isset( $_GET['test'] ) ) { + return; + } + + $float = doubleval( $_GET['test'] ); // OK. + $count = count( $_GET['test'] ); // Issue #1659; OK. +} + +function test_allow_array_key_exists_alias() { + if ( key_exists( 'my_field1', $_POST ) ) { + $id = (int) $_POST['my_field1']; // OK. +} + +function test_correct_multi_level_array_validation() { + if ( isset( $_POST['toplevel']['sublevel'] ) ) { + $id = (int) $_POST['toplevel']; // OK, if a subkey exists, the top level key *must* also exist. + $id = (int) $_POST['toplevel']['sublevel']; // OK. + $id = (int) $_POST['toplevel']['sublevel']['subsub']; // Bad x1 - validate, this sub has not been validated. + } + + if ( array_key_exists( 'bar', $_POST['foo'] ) ) { + $id = (int) $_POST['bar']; // Bad x 1 - validate. + $id = (int) $_POST['foo']; // OK. + $id = (int) $_POST['foo']['bar']; // OK. + $id = (int) $_POST['foo']['bar']['baz']; // Bad x 1 - validate. + } +} + +function test_correct_multi_level_array_validation_key_order() { + if ( isset( $_POST['toplevel']['sublevel'] ) ) { + $id = (int) $_POST['sublevel']['toplevel']; // Bad x 1 - validate - key order is wrong. + } +} + +function test_invalid_array_key_exists_call() { + if ( array_key_exists( 'bar' ) ) { + $id = (int) $_POST['bar']; // Bad x 1 - validate. + } +} + +function test_ignoring_is_type_function_calls() { + // Usage within variable type tests does not need unslashing or sanitization. + if ( isset( $_POST[ $key ] ) && is_numeric( $_POST[ $key ] ) ) {} // OK. + if ( isset($_POST['plugin']) && is_string( $_POST['plugin'] ) ) {} // OK. + + if ( ! is_null( $_GET['not_set'] ) ) {} // Bad x1 - missing validation. + if ( array_key_exists( 'null', $_GET ) && ! is_null( $_GET['null'] ) ) {} // OK. + if ( array_key_exists( 'null', $_POST ) && $_POST['null'] !== null ) {} // OK. +} + +function test_additional_array_walking_functions() { + if ( ! isset( $_GET['test'] ) ) { + return; + } + + $sane = map_deep( wp_unslash( $_GET['test'] ), 'sanitize_text_field' ); // Ok. + $sane = map_deep( wp_unslash( $_GET['test'] ), 'foo' ); // Bad. +} + +function test_recognize_array_comparison_functions_as_such() { + if ( ! isset( $_POST['form_fields'] ) ) { + return; + } + + if ( isset( $_REQUEST['plugin_status'] ) && in_array( $_REQUEST['plugin_status'], array( 'install', 'update', 'activate' ), true ) ) {} // OK. + if ( isset( $_REQUEST['plugin_state'] ) && in_array( $_REQUEST['plugin_state'], $states, true ) ) {} // OK. + if ( in_array( 'my_form_hidden_field_value', $_POST['form_fields'], true ) ) {} // OK. + if ( array_search( $_POST['form_fields'], 'my_form_hidden_field_value' ) ) {} // OK. + if ( array_keys( $_POST['form_fields'], 'my_form_hidden_field_value', true ) ) {} // OK. + if ( array_keys( $_POST['form_fields'] ) ) {} // Bad x2. +} + +/* + * Test recognition of validation via null coalesce, while still checking the var for sanitization. + */ +function test_null_coalesce_1() { + $var = sanitize_text_field( wp_unslash( $_POST['foo'] ?? '' ) ); // OK. + $var = sanitize_text_field( wp_unslash( $_POST['fool'] ?? $_POST['secondary'] ?? '' ) ); // OK. + $var = sanitize_text_field( wp_unslash( $_POST['bar']['sub'] ?? '' ) ); // OK. + $var = sanitize_text_field( $_POST['foobar'] ?? '' ); // Bad x1 - unslash. +} + +// The below two sets should give the same errors. +function test_null_coalesce_2() { + $var = $_POST['foo'] ?? ''; // Bad x2 - sanitize + unslash. + $var = $_POST['bar']['sub'] ?? ''; // Bad x2 - sanitize + unslash. + $var = ( $_POST['foobar']['sub'] ?? '' ); // Bad x2 - sanitize + unslash. + + $var = isset( $_POST['_foo'] ) ? $_POST['_foo'] : ''; // Bad x2 - sanitize + unslash. + $var = isset( $_POST['_bar']['_sub'] ) ? $_POST['_bar']['_sub'] : ''; // Bad x2 - sanitize + unslash. + $var = ( isset( $_POST['_foobar']['_sub'] ) ? $_POST['_foobar']['_sub'] : '' ); // Bad x2 - sanitize + unslash. +} + +function test_null_coalesce_validation() { + $_POST['key'] = $_POST['key'] ?? 'default'; // OK, assignment & Bad x2 - unslash, sanitize. + $key = sanitize_text_field( wp_unslash( $_POST['key'] ) ); // OK, validated via null coalesce. + $another_key = sanitize_text_field( wp_unslash( $_POST['another_key'] ) ); // Bad, not validated, different key. +} + +function test_null_coalesce_equals_validation() { + $_POST['key'] ??= 'default'; // OK, assignment. + $key = sanitize_text_field( wp_unslash( $_POST['key'] ) ); // OK, validated via null coalesce equals. + $another_key = sanitize_text_field( wp_unslash( $_POST['another_key'] ) ); // Bad, not validated, different key. +} + +function test_using_different_unslashing_functions() { + if ( ! isset( $_GET['test'] ) ) { + return; + } + + $sane = sanitize_text_field(stripslashes_deep($_GET['test'])); // Ok. + $sane = sanitize_text_field( stripslashes_from_strings_only( $_GET['test'] ) ); // OK. +} diff --git a/WordPress/Tests/Security/ValidatedSanitizedInputUnitTest.php b/WordPress/Tests/Security/ValidatedSanitizedInputUnitTest.php index 02b7e9d4ae..ca0821bd59 100644 --- a/WordPress/Tests/Security/ValidatedSanitizedInputUnitTest.php +++ b/WordPress/Tests/Security/ValidatedSanitizedInputUnitTest.php @@ -54,6 +54,31 @@ public function getErrorList() { 138 => 1, 150 => 2, 160 => 2, + 164 => 2, + 189 => 1, + 202 => 1, + 206 => 1, + 210 => 1, + 216 => 1, + 217 => 1, + 238 => 1, + 242 => 1, + 245 => 1, + 251 => 1, + 257 => 1, + 266 => 1, + 277 => 1, + 290 => 2, + 300 => 1, + 305 => 2, + 306 => 2, + 307 => 2, + 309 => 2, + 310 => 2, + 311 => 2, + 315 => 2, + 317 => 1, + 323 => 1, ); } diff --git a/WordPress/Tests/WP/AlternativeFunctionsUnitTest.inc b/WordPress/Tests/WP/AlternativeFunctionsUnitTest.inc index ca2989c033..a9646d98e9 100644 --- a/WordPress/Tests/WP/AlternativeFunctionsUnitTest.inc +++ b/WordPress/Tests/WP/AlternativeFunctionsUnitTest.inc @@ -50,3 +50,21 @@ file_get_contents(MYABSPATH . 'plugin-file.json'); // Warning. file_get_contents( MUPLUGINDIR . 'some-file.xml' ); // OK. file_get_contents( plugin_dir_path( __FILE__ ) . 'subfolder/*.conf' ); // OK. file_get_contents(WP_Upload_Dir()['path'] . 'subdir/file.inc'); // OK. +file_get_contents( 'php://input' ); // OK. + +// Loosely related to issue 295. +file_get_contents( 'php://stdin' ); // OK. +$input_stream = fopen( 'php://stdin', 'w' ); // OK. +$csv_ar = fopen(STDIN); // OK. + +$output_stream = fopen( 'php://output', 'w' ); // OK. +$output_stream = fopen( 'php://stdout', 'w' ); // OK. +$output_stream = fopen( 'php://stderr', 'w' ); // OK. +$output_stream = fopen( STDOUT, 'w' ); // OK. +$output_stream = fopen( STDERR, 'w' ); // OK. +$output_stream = fopen( 'php://fd/3', 'w' ); // OK. +$fp = fopen("php://temp/maxmemory:$fiveMBs", 'r+'); // OK. +readfile( 'php://filter/resource=http://www.example.com' ); // Warning. +file_put_contents("php://filter/write=string.rot13/resource=example.txt","Hello World"); // Warning. + +curl_version(); // OK. diff --git a/WordPress/Tests/WP/AlternativeFunctionsUnitTest.php b/WordPress/Tests/WP/AlternativeFunctionsUnitTest.php index 36ea1bcb51..55107f0f6a 100644 --- a/WordPress/Tests/WP/AlternativeFunctionsUnitTest.php +++ b/WordPress/Tests/WP/AlternativeFunctionsUnitTest.php @@ -40,13 +40,9 @@ public function getWarningList() { 5 => 1, 6 => 1, 7 => 1, - 10 => 1, - 12 => 1, - 14 => 1, - 16 => 1, 17 => 1, 18 => 1, @@ -60,13 +56,13 @@ public function getWarningList() { 26 => 1, 27 => 1, 28 => 1, - 40 => 1, - 44 => 1, 46 => 1, 47 => 1, 49 => 1, + 67 => 1, + 68 => 1, ); } diff --git a/WordPress/Tests/WP/CronIntervalUnitTest.inc b/WordPress/Tests/WP/CronIntervalUnitTest.inc index 712eb64072..19b061fe34 100644 --- a/WordPress/Tests/WP/CronIntervalUnitTest.inc +++ b/WordPress/Tests/WP/CronIntervalUnitTest.inc @@ -52,7 +52,7 @@ add_filter( 'cron_schedules' ); // Ignore, no callback parameter. add_filter( 'cron_schedules', [ 'Foo', 'my_add_quicklier' ] ); // Error: 5 min. -// Ignore, not our function. Currently gives false positive, will be fixed later when function call detection utility functions are added. +// Ignore, not our function. My_Custom::add_filter( 'cron_schedules', [ 'Foo', 'my_add_quicklier' ] ); // Deal correctly with the WP time constants. @@ -139,3 +139,8 @@ add_filter( 'cron_schedules', function ( $schedules ) { ); return $schedules; } ); // Error: 9 min. + +Custom::add_filter( 'cron_schedules', array( $class, $method ) ); // OK, not the WP function. +add_filter( 'some_hook', array( $place, 'cron_schedules' ) ); // OK, not the hook we're looking for. +add_filter( function() { return get_hook_name('cron_schedules'); }(), array( $class, $method ) ); // OK, nested in another function call. + diff --git a/WordPress/Tests/WP/CronIntervalUnitTest.php b/WordPress/Tests/WP/CronIntervalUnitTest.php index be5cc9f6b8..704923cc5f 100644 --- a/WordPress/Tests/WP/CronIntervalUnitTest.php +++ b/WordPress/Tests/WP/CronIntervalUnitTest.php @@ -45,7 +45,6 @@ public function getWarningList() { 41 => 1, 43 => 1, 53 => 1, - 56 => 1, // False positive. 67 => 1, 76 => 1, 85 => 1, diff --git a/WordPress/Tests/WP/DeprecatedFunctionsUnitTest.inc b/WordPress/Tests/WP/DeprecatedFunctionsUnitTest.inc index 55b5107e2f..eb0822692e 100644 --- a/WordPress/Tests/WP/DeprecatedFunctionsUnitTest.inc +++ b/WordPress/Tests/WP/DeprecatedFunctionsUnitTest.inc @@ -143,6 +143,7 @@ get_user_details(); get_usermeta(); get_usernumposts(); graceful_fail(); +install_blog_defaults(); is_main_blog(); is_site_admin(); is_taxonomy(); @@ -319,10 +320,6 @@ use function popuplinks as something_else; // Related to issue #1306. post_form_autocomplete_off(); wp_embed_handler_googlevideo(); wp_get_sites(); - -/* - * Warning. - */ /* ============ WP 4.7 ============ */ _sort_nav_menu_items(); _usort_terms_by_ID(); @@ -330,6 +327,10 @@ _usort_terms_by_name(); get_paged_template(); wp_get_network(); wp_kses_js_entities(); + +/* + * Warning. + */ /* ============ WP 4.8 ============ */ wp_dashboard_plugins_output(); /* ============ WP 4.9 ============ */ @@ -337,3 +338,6 @@ get_shortcut_link(); is_user_option_local(); wp_ajax_press_this_add_category(); wp_ajax_press_this_save_post(); +/* ============ WP 5.1 ============ */ +insert_blog(); +install_blog(); diff --git a/WordPress/Tests/WP/DeprecatedFunctionsUnitTest.php b/WordPress/Tests/WP/DeprecatedFunctionsUnitTest.php index 5fbce22ce0..c14a79c9bd 100644 --- a/WordPress/Tests/WP/DeprecatedFunctionsUnitTest.php +++ b/WordPress/Tests/WP/DeprecatedFunctionsUnitTest.php @@ -28,7 +28,7 @@ class DeprecatedFunctionsUnitTest extends AbstractSniffUnitTest { */ public function getErrorList() { - $errors = array_fill( 8, 314, 1 ); + $errors = array_fill( 8, 322, 1 ); // Unset the lines related to version comments. unset( @@ -45,22 +45,23 @@ public function getErrorList() { $errors[80], $errors[118], $errors[125], - $errors[161], - $errors[174], - $errors[178], - $errors[210], - $errors[233], - $errors[251], - $errors[255], - $errors[262], - $errors[274], - $errors[281], - $errors[285], - $errors[290], - $errors[295], - $errors[303], - $errors[310], - $errors[318] + $errors[162], + $errors[175], + $errors[179], + $errors[211], + $errors[234], + $errors[252], + $errors[256], + $errors[263], + $errors[275], + $errors[282], + $errors[286], + $errors[291], + $errors[296], + $errors[304], + $errors[311], + $errors[319], + $errors[323] ); return $errors; @@ -73,10 +74,10 @@ public function getErrorList() { */ public function getWarningList() { - $warnings = array_fill( 326, 14, 1 ); + $warnings = array_fill( 335, 9, 1 ); // Unset the lines related to version comments. - unset( $warnings[326], $warnings[333], $warnings[335] ); + unset( $warnings[336], $warnings[341] ); return $warnings; } diff --git a/WordPress/Tests/WP/DeprecatedParametersUnitTest.inc b/WordPress/Tests/WP/DeprecatedParametersUnitTest.inc index 7e35ad422a..8022d59bc5 100644 --- a/WordPress/Tests/WP/DeprecatedParametersUnitTest.inc +++ b/WordPress/Tests/WP/DeprecatedParametersUnitTest.inc @@ -48,6 +48,7 @@ the_author( 'deprecated', 'deprecated' ); the_author_posts_link( 'deprecated' ); trackback_rdf( 'deprecated' ); trackback_url( 'deprecated' ); +unregister_setting( '', '', '', 'deprecated' ); update_blog_option( '', '', '', 'deprecated' ); update_user_status( '', '', '', 'deprecated' ); wp_get_http_headers( '', 'deprecated' ); @@ -60,7 +61,6 @@ wp_title_rss( 'deprecated' ); wp_upload_bits( '', 'deprecated' ); xfn_check( '', '', 'deprecated' ); -// All will give an WARNING as they have been deprecated after WP 4.7. +// All will give an WARNING as they have been deprecated after WP 4.8. get_category_parents( '', '', '', '', array( 'deprecated') ); -unregister_setting( '', '', '', 'deprecated' ); diff --git a/WordPress/Tests/WP/DeprecatedParametersUnitTest.php b/WordPress/Tests/WP/DeprecatedParametersUnitTest.php index ffbc573474..77d4d87e88 100644 --- a/WordPress/Tests/WP/DeprecatedParametersUnitTest.php +++ b/WordPress/Tests/WP/DeprecatedParametersUnitTest.php @@ -27,7 +27,7 @@ class DeprecatedParametersUnitTest extends AbstractSniffUnitTest { * @return array => */ public function getErrorList() { - $errors = array_fill( 28, 34, 1 ); + $errors = array_fill( 28, 35, 1 ); $errors[22] = 1; $errors[23] = 1; @@ -47,7 +47,6 @@ public function getErrorList() { */ public function getWarningList() { return array( - 65 => 1, 66 => 1, ); } diff --git a/WordPress/Tests/WP/DiscouragedConstantsUnitTest.inc b/WordPress/Tests/WP/DiscouragedConstantsUnitTest.inc index d5e5f3a689..4949a9557a 100644 --- a/WordPress/Tests/WP/DiscouragedConstantsUnitTest.inc +++ b/WordPress/Tests/WP/DiscouragedConstantsUnitTest.inc @@ -42,7 +42,7 @@ define( 'My\STYLESHEETPATH', 'something' ); if ( defined( 'STYLESHEETPATH' ) ) { // Ok. // Do something unrelated. } - +echo namespace\STYLESHEETPATH; // "Magic" namespace operator. /* * These are all bad. diff --git a/WordPress/Tests/WP/EnqueuedResourcesUnitTest.inc b/WordPress/Tests/WP/EnqueuedResourcesUnitTest.inc index 941eacd779..3b5ec4f191 100644 --- a/WordPress/Tests/WP/EnqueuedResourcesUnitTest.inc +++ b/WordPress/Tests/WP/EnqueuedResourcesUnitTest.inc @@ -30,3 +30,9 @@ $head = <<<'EOD' EOD; + +?> + +jQuery( document ).ready( function() { + $('link[rel="stylesheet"]:not([data-inprogress])').forEach(StyleFix.link); +}); diff --git a/composer.json b/composer.json index 7e6f35545b..f306ca7bdc 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" }, "suggest": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.4.3 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically." + "dealerdirect/phpcodesniffer-composer-installer": "^0.5.0 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically." }, "minimum-stability": "RC", "scripts": { diff --git a/phpcs.xml.dist.sample b/phpcs.xml.dist.sample index ef8dae68b1..ffe70c7f1a 100644 --- a/phpcs.xml.dist.sample +++ b/phpcs.xml.dist.sample @@ -70,7 +70,7 @@ the wiki: https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/wiki/Customizable-sniff-properties --> - +