├── WordPressVIPMinimum ├── Sniffs │ ├── Sniff.php │ ├── Security │ │ ├── VuejsSniff.php │ │ ├── ExitAfterRedirectSniff.php │ │ ├── MustacheSniff.php │ │ ├── PHPFilterFunctionsSniff.php │ │ ├── EscapingVoidReturnFunctionsSniff.php │ │ ├── UnderscorejsSniff.php │ │ ├── StaticStrreplaceSniff.php │ │ └── ProperEscapingFunctionSniff.php │ ├── Performance │ │ ├── RemoteRequestTimeoutSniff.php │ │ ├── RegexpCompareSniff.php │ │ ├── NoPagingSniff.php │ │ ├── OrderByRandSniff.php │ │ ├── WPQueryParamsSniff.php │ │ ├── FetchingRemoteDataSniff.php │ │ ├── TaxonomyMetaInOptionsSniff.php │ │ ├── CacheValueOverrideSniff.php │ │ └── LowExpiryCacheTimeSniff.php │ ├── Variables │ │ ├── RestrictedVariablesSniff.php │ │ └── ServerVariablesSniff.php │ ├── Classes │ │ └── RestrictedExtendClassesSniff.php │ ├── Constants │ │ ├── ConstantStringSniff.php │ │ └── RestrictedConstantsSniff.php │ ├── JS │ │ ├── DangerouslySetInnerHTMLSniff.php │ │ ├── StrippingTagsSniff.php │ │ ├── InnerHTMLSniff.php │ │ ├── StringConcatSniff.php │ │ ├── WindowSniff.php │ │ └── HTMLExecutingFunctionsSniff.php │ ├── Files │ │ ├── IncludingNonPHPFileSniff.php │ │ └── IncludingFileSniff.php │ ├── Functions │ │ ├── StripTagsSniff.php │ │ ├── DynamicCallsSniff.php │ │ ├── CheckReturnValueSniff.php │ │ └── RestrictedFunctionsSniff.php │ ├── Hooks │ │ ├── RestrictedHooksSniff.php │ │ └── AlwaysReturnInFilterSniff.php │ └── AbstractVariableRestrictionsSniff.php ├── ruleset-test.php └── ruleset.xml ├── composer.json ├── README.md └── WordPress-VIP-Go └── ruleset-test.php /WordPressVIPMinimum/Sniffs/Sniff.php: -------------------------------------------------------------------------------- 1 | 29 | */ 30 | public function register() { 31 | return [ 32 | T_CONSTANT_ENCAPSED_STRING, 33 | T_INLINE_HTML, 34 | ]; 35 | } 36 | 37 | /** 38 | * Processes this test, when one of its tokens is encountered. 39 | * 40 | * @param int $stackPtr The position of the current token in the stack passed in $tokens. 41 | * 42 | * @return void 43 | */ 44 | public function process_token( $stackPtr ) { 45 | 46 | if ( strpos( $this->tokens[ $stackPtr ]['content'], 'v-html' ) !== false ) { 47 | // Vue autoescape disabled. 48 | $message = 'Found Vue.js non-escaped (raw) HTML directive.'; 49 | $this->phpcsFile->addWarning( $message, $stackPtr, 'RawHTMLDirectiveFound' ); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /WordPressVIPMinimum/Sniffs/Performance/RemoteRequestTimeoutSniff.php: -------------------------------------------------------------------------------- 1 | >> 23 | */ 24 | public function getGroups() { 25 | return [ 26 | 'timeout' => [ 27 | 'type' => 'error', 28 | 'message' => 'Detected high remote request timeout. `%s` is set to `%d`.', 29 | 'keys' => [ 30 | 'timeout', 31 | ], 32 | ], 33 | ]; 34 | } 35 | 36 | /** 37 | * Callback to process each confirmed key, to check value. 38 | * 39 | * @param string $key Array index / key. 40 | * @param mixed $val Assigned value. 41 | * @param int $line Token line. 42 | * @param array $group Group definition. 43 | * 44 | * @return bool FALSE if no match, TRUE if matches. 45 | */ 46 | public function callback( $key, $val, $line, $group ) { 47 | return (int) $val > 3; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /WordPressVIPMinimum/Sniffs/Performance/RegexpCompareSniff.php: -------------------------------------------------------------------------------- 1 | >> 24 | */ 25 | public function getGroups() { 26 | return [ 27 | 'compare' => [ 28 | 'type' => 'error', 29 | 'message' => 'Detected regular expression comparison. `%s` is set to `%s`.', 30 | 'keys' => [ 31 | 'compare', 32 | 'meta_compare', 33 | ], 34 | ], 35 | ]; 36 | } 37 | 38 | /** 39 | * Callback to process each confirmed key, to check value. 40 | * 41 | * @param string $key Array index / key. 42 | * @param mixed $val Assigned value. 43 | * @param int $line Token line. 44 | * @param array $group Group definition. 45 | * 46 | * @return bool FALSE if no match, TRUE if matches. 47 | */ 48 | public function callback( $key, $val, $line, $group ) { 49 | $val = TextStrings::stripQuotes( $val ); 50 | return ( strpos( $val, 'NOT REGEXP' ) === 0 51 | || strpos( $val, 'REGEXP' ) === 0 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /WordPressVIPMinimum/Sniffs/Performance/NoPagingSniff.php: -------------------------------------------------------------------------------- 1 | >> 26 | */ 27 | public function getGroups() { 28 | return [ 29 | 'nopaging' => [ 30 | 'type' => 'error', 31 | 'message' => 'Disabling pagination is prohibited in VIP context, do not set `%s` to `%s` ever.', 32 | 'keys' => [ 33 | 'nopaging', 34 | ], 35 | ], 36 | ]; 37 | } 38 | 39 | /** 40 | * Callback to process each confirmed key, to check value. 41 | * 42 | * @param string $key Array index / key. 43 | * @param mixed $val Assigned value. 44 | * @param int $line Token line. 45 | * @param array $group Group definition. 46 | * 47 | * @return bool FALSE if no match, TRUE if matches. 48 | */ 49 | public function callback( $key, $val, $line, $group ) { 50 | $key = strtolower( $key ); 51 | 52 | return ( $key === 'nopaging' && ( $val === 'true' || $val === '1' ) ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /WordPressVIPMinimum/Sniffs/Performance/OrderByRandSniff.php: -------------------------------------------------------------------------------- 1 | rand. 18 | * 19 | * @link https://docs.wpvip.com/technical-references/code-review/vip-errors/#h-order-by-rand 20 | */ 21 | class OrderByRandSniff extends AbstractArrayAssignmentRestrictionsSniff { 22 | 23 | /** 24 | * Groups of variables to restrict. 25 | * 26 | * @return array>> 27 | */ 28 | public function getGroups() { 29 | return [ 30 | 'orderby' => [ 31 | 'type' => 'error', 32 | 'message' => 'Detected forbidden query_var "%s" of %s. Use vip_get_random_posts() instead.', 33 | 'keys' => [ 34 | 'orderby', 35 | ], 36 | ], 37 | ]; 38 | } 39 | 40 | /** 41 | * Callback to process each confirmed key, to check value 42 | * 43 | * @param string $key Array index / key. 44 | * @param mixed $val Assigned value. 45 | * @param int $line Token line. 46 | * @param array $group Group definition. 47 | * 48 | * @return bool FALSE if no match, TRUE if matches. 49 | */ 50 | public function callback( $key, $val, $line, $group ) { 51 | $val = TextStrings::stripQuotes( $val ); 52 | return strtolower( $val ) === 'rand'; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /WordPressVIPMinimum/Sniffs/Variables/RestrictedVariablesSniff.php: -------------------------------------------------------------------------------- 1 | array( 24 | * 'wpdb' => array( 25 | * 'type' => 'error' | 'warning', 26 | * 'message' => 'Dont use this one please!', 27 | * 'variables' => array( '$val', '$var' ), 28 | * 'object_vars' => array( '$foo->bar', .. ), 29 | * 'array_members' => array( '$foo['bar']', .. ), 30 | * ) 31 | * ) 32 | * 33 | * @return array>> 34 | */ 35 | public function getGroups() { 36 | return [ 37 | 'user_meta' => [ 38 | 'type' => 'error', 39 | 'message' => 'Usage of users tables is highly discouraged in VIP context', 40 | 'object_vars' => [ 41 | '$wpdb->users', 42 | ], 43 | ], 44 | 'session' => [ 45 | 'type' => 'error', 46 | 'message' => 'Usage of $_SESSION variable is prohibited.', 47 | 'variables' => [ 48 | '$_SESSION', 49 | ], 50 | ], 51 | 52 | // @link https://docs.wpvip.com/technical-references/code-review/vip-errors/#h-cache-constraints 53 | 'cache_constraints' => [ 54 | 'type' => 'warning', 55 | 'message' => 'Due to server-side caching, server-side based client related logic might not work. We recommend implementing client side logic in JavaScript instead.', 56 | 'variables' => [ 57 | '$_COOKIE', 58 | ], 59 | 'array_members' => [ 60 | '$_SERVER[\'HTTP_USER_AGENT\']', 61 | '$_SERVER[\'REMOTE_ADDR\']', 62 | ], 63 | ], 64 | ]; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /WordPressVIPMinimum/Sniffs/Classes/RestrictedExtendClassesSniff.php: -------------------------------------------------------------------------------- 1 | >> 26 | */ 27 | public function getGroups() { 28 | return [ 29 | 'wp_cli' => [ 30 | 'type' => 'warning', 31 | 'message' => 'We recommend extending `WPCOM_VIP_CLI_Command` instead of `WP_CLI_Command` and using the helper functions available in it (such as `stop_the_insanity()`), see https://vip.wordpress.com/documentation/writing-bin-scripts/ for more information.', 32 | 'classes' => [ 33 | 'WP_CLI_Command', 34 | ], 35 | ], 36 | ]; 37 | } 38 | 39 | /** 40 | * Returns an array of tokens this test wants to listen for. 41 | * 42 | * @return array 43 | */ 44 | public function register() { 45 | $targets = parent::register(); 46 | if ( empty( $targets ) ) { 47 | return $targets; 48 | } 49 | 50 | return [ T_EXTENDS ]; 51 | } 52 | 53 | /** 54 | * Process a matched token. 55 | * 56 | * @param int $stackPtr The position of the current token in the stack. 57 | * @param string $group_name The name of the group which was matched. 58 | * @param string $matched_content The token content (class name) which was matched 59 | * in lowercase. 60 | * 61 | * @return void 62 | */ 63 | public function process_matched_token( $stackPtr, $group_name, $matched_content ) { 64 | foreach ( $this->getGroups() as $group => $group_args ) { 65 | $isError = ( $group_args['type'] === 'error' ); 66 | MessageHelper::addMessage( $this->phpcsFile, $group_args['message'], $stackPtr, $isError, $group ); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /WordPressVIPMinimum/Sniffs/Security/ExitAfterRedirectSniff.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | public function register() { 26 | return [ T_STRING ]; 27 | } 28 | 29 | /** 30 | * Process this test when one of its tokens is encountered 31 | * 32 | * @param int $stackPtr The position of the current token in the stack passed in $tokens. 33 | * 34 | * @return void 35 | */ 36 | public function process_token( $stackPtr ) { 37 | 38 | if ( $this->tokens[ $stackPtr ]['content'] !== 'wp_redirect' && $this->tokens[ $stackPtr ]['content'] !== 'wp_safe_redirect' ) { 39 | return; 40 | } 41 | 42 | $openBracket = $this->phpcsFile->findNext( Tokens::$emptyTokens, $stackPtr + 1, null, true ); 43 | 44 | if ( $this->tokens[ $openBracket ]['code'] !== T_OPEN_PARENTHESIS ) { 45 | return; 46 | } 47 | 48 | $next_token = $this->phpcsFile->findNext( array_merge( Tokens::$emptyTokens, [ T_SEMICOLON, T_CLOSE_PARENTHESIS ] ), $this->tokens[ $openBracket ]['parenthesis_closer'] + 1, null, true ); 49 | 50 | $message = '`%s()` should almost always be followed by a call to `exit;`.'; 51 | $data = [ $this->tokens[ $stackPtr ]['content'] ]; 52 | 53 | if ( $this->tokens[ $next_token ]['code'] === T_OPEN_CURLY_BRACKET ) { 54 | $is_exit_in_scope = false; 55 | for ( $i = $this->tokens[ $next_token ]['scope_opener']; $i <= $this->tokens[ $next_token ]['scope_closer']; $i++ ) { 56 | if ( $this->tokens[ $i ]['code'] === T_EXIT ) { 57 | $is_exit_in_scope = true; 58 | } 59 | } 60 | if ( $is_exit_in_scope === false ) { 61 | $this->phpcsFile->addError( $message, $stackPtr, 'NoExitInConditional', $data ); 62 | } 63 | } elseif ( $this->tokens[ $next_token ]['code'] !== T_EXIT ) { 64 | $this->phpcsFile->addError( $message, $stackPtr, 'NoExit', $data ); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /WordPressVIPMinimum/Sniffs/Constants/ConstantStringSniff.php: -------------------------------------------------------------------------------- 1 | Key is the function name, value irrelevant. 32 | */ 33 | protected $target_functions = [ 34 | 'define' => true, 35 | 'defined' => true, 36 | ]; 37 | 38 | /** 39 | * Process the parameters of a matched function. 40 | * 41 | * @param int $stackPtr The position of the current token in the stack. 42 | * @param string $group_name The name of the group which was matched. 43 | * @param string $matched_content The token content (function name) which was matched 44 | * in lowercase. 45 | * @param array $parameters Array with information about the parameters. 46 | * 47 | * @return void 48 | */ 49 | public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) { 50 | $param = PassedParameters::getParameterFromStack( $parameters, 1, 'constant_name' ); 51 | if ( $param === false ) { 52 | // Target parameter not found. 53 | return; 54 | } 55 | 56 | $search = Tokens::$emptyTokens; 57 | $search[ T_STRING ] = T_STRING; 58 | 59 | $has_only_tstring = $this->phpcsFile->findNext( $search, $param['start'], $param['end'] + 1, true ); 60 | if ( $has_only_tstring !== false ) { 61 | // Came across something other than a T_STRING token. Ignore. 62 | return; 63 | } 64 | 65 | $tstring_token = $this->phpcsFile->findNext( T_STRING, $param['start'], $param['end'] + 1 ); 66 | 67 | $message = 'The `%s()` function expects to be passed the constant name as a text string.'; 68 | $data = [ $this->tokens[ $stackPtr ]['content'] ]; 69 | $this->phpcsFile->addError( $message, $tstring_token, 'NotCheckingConstantName', $data ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "automattic/vipwpcs", 3 | "type": "phpcodesniffer-standard", 4 | "description": "PHP_CodeSniffer rules (sniffs) to enforce WordPress VIP minimum coding conventions", 5 | "keywords": [ 6 | "phpcs", 7 | "static analysis", 8 | "standards", 9 | "WordPress" 10 | ], 11 | "license": "MIT", 12 | "authors": [ 13 | { 14 | "name": "Contributors", 15 | "homepage": "https://github.com/Automattic/VIP-Coding-Standards/graphs/contributors" 16 | } 17 | ], 18 | "require": { 19 | "php": ">=5.4", 20 | "phpcsstandards/phpcsextra": "^1.4.0", 21 | "phpcsstandards/phpcsutils": "^1.1.0", 22 | "sirbrillig/phpcs-variable-analysis": "^2.12.0", 23 | "squizlabs/php_codesniffer": "^3.13.2", 24 | "wp-coding-standards/wpcs": "^3.2.0" 25 | }, 26 | "require-dev": { 27 | "php-parallel-lint/php-parallel-lint": "^1.4.0", 28 | "php-parallel-lint/php-console-highlighter": "^1.0.0", 29 | "phpcompatibility/php-compatibility": "^9", 30 | "phpcsstandards/phpcsdevtools": "^1.2.3", 31 | "phpunit/phpunit": "^4 || ^5 || ^6 || ^7 || ^8 || ^9" 32 | }, 33 | "config": { 34 | "allow-plugins": { 35 | "dealerdirect/phpcodesniffer-composer-installer": true 36 | }, 37 | "lock": false 38 | }, 39 | "scripts": { 40 | "test-ruleset": "bin/ruleset-tests", 41 | "lint": [ 42 | "bin/php-lint" 43 | ], 44 | "cs": "@php ./vendor/squizlabs/php_codesniffer/bin/phpcs", 45 | "test": "bin/unit-tests", 46 | "test-coverage": "bin/unit-tests-coverage", 47 | "feature-completeness": [ 48 | "@php ./vendor/phpcsstandards/phpcsdevtools/bin/phpcs-check-feature-completeness -q ./WordPressVIPMinimum" 49 | ], 50 | "check": [ 51 | "@lint", 52 | "@test-ruleset", 53 | "@test", 54 | "@cs", 55 | "@feature-completeness" 56 | ] 57 | }, 58 | "scripts-descriptions": { 59 | "lint": "VIPCS: Lint PHP files against parse errors.", 60 | "cs": "VIPCS: Check the code style and code quality of the codebase via PHPCS.", 61 | "test": "VIPCS: Run the unit tests for the VIPCS sniffs.", 62 | "test-coverage": "VIPCS: Run the unit tests for the VIPCS sniffs with coverage enabled.", 63 | "test-ruleset": "VIPCS: Run the ruleset tests for the VIPCS sniffs.", 64 | "feature-completeness": "VIPCS: Check if all the VIPCS sniffs have tests.", 65 | "check": "VIPCS: Run all checks (lint, CS, feature completeness) and tests." 66 | }, 67 | "support": { 68 | "issues": "https://github.com/Automattic/VIP-Coding-Standards/issues", 69 | "wiki": "https://github.com/Automattic/VIP-Coding-Standards/wiki", 70 | "source": "https://github.com/Automattic/VIP-Coding-Standards" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VIP Coding Standards 2 | 3 | This project contains [PHP_CodeSniffer (PHPCS) sniffs and rulesets](https://github.com/PHPCSStandards/PHP_CodeSniffer) to validate code developed for [WordPress VIP](https://wpvip.com/). 4 | 5 | This project contains two rulesets: 6 | 7 | - `WordPressVIPMinimum` - for use with projects on the (older) WordPress.com VIP platform. 8 | - `WordPress-VIP-Go` - for use with projects on the (newer) VIP Go platform. 9 | 10 | These rulesets contain only the rules which are considered to be [errors](https://docs.wpvip.com/php_codesniffer/errors/) and [warnings](https://docs.wpvip.com/php_codesniffer/warnings/) according to the WordPress VIP documentation. 11 | 12 | The rulesets use rules from the [WordPress Coding Standards](https://github.com/WordPress/WordPress-Coding-Standards) (WPCS) project, as well as the [VariableAnalysis](https://github.com/sirbrillig/phpcs-variable-analysis) standard. 13 | 14 | [Learn](https://docs.wpvip.com/vip-code-analysis-bot/phpcs-report/) about why violations are flagged as errors vs warnings and what the levels mean. 15 | 16 | ## Minimal requirements 17 | 18 | * PHP 5.4+ 19 | * [PHPCS 3.13.2+](https://github.com/PHPCSStandards/PHP_CodeSniffer/releases) 20 | * [PHPCSUtils 1.1.0+](https://github.com/PHPCSStandards/PHPCSUtils) 21 | * [PHPCSExtra 1.4.0+](https://github.com/PHPCSStandards/PHPCSExtra) 22 | * [WPCS 3.1.0+](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/releases) 23 | * [VariableAnalysis 2.12.0+](https://github.com/sirbrillig/phpcs-variable-analysis/releases) 24 | 25 | ## Installation 26 | 27 | [Composer](https://getcomposer.org/) will install the latest compatible versions of PHPCS, PHPCSUtils, PHPCSExtra, WPCS and VariableAnalysis and register the external standards with PHP_CodeSniffer. 28 | 29 | Please refer to the [installation instructions for installing PHP_CodeSniffer for WordPress VIP](https://docs.wpvip.com/how-tos/code-review/php_codesniffer/) for more details. 30 | 31 | As of VIPCS version 2.3.0, there is no need to `require` the [PHP_CodeSniffer Standards Composer Installer Plugin](https://github.com/PHPCSStandards/composer-installer) anymore as it is now a requirement of VIPCS itself. Permission to run the plugin will still need to be granted though when using Composer 2.2 or higher. 32 | 33 | ### Composer Project-based Installation 34 | 35 | To install the VIP Coding Standards, run the following from the root of your project: 36 | 37 | ```bash 38 | composer config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true 39 | composer require --dev automattic/vipwpcs 40 | ``` 41 | 42 | ### Composer Global Installation 43 | 44 | Alternatively, it can be installed standard globally for use across multiple projects: 45 | 46 | ```bash 47 | composer global config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true 48 | composer global require --dev automattic/vipwpcs 49 | ``` 50 | 51 | ## Contribution 52 | 53 | Please see [CONTRIBUTION.md](.github/CONTRIBUTING.md). 54 | 55 | ## License 56 | 57 | Licensed under [GPL-2.0-or-later](LICENSE.md). 58 | -------------------------------------------------------------------------------- /WordPressVIPMinimum/Sniffs/JS/DangerouslySetInnerHTMLSniff.php: -------------------------------------------------------------------------------- 1 | 34 | */ 35 | public function register() { 36 | return [ 37 | T_STRING, 38 | ]; 39 | } 40 | 41 | /** 42 | * Processes this test, when one of its tokens is encountered. 43 | * 44 | * @param int $stackPtr The position of the current token in the stack passed in $tokens. 45 | * 46 | * @return void 47 | */ 48 | public function process_token( $stackPtr ) { 49 | 50 | if ( $this->tokens[ $stackPtr ]['content'] !== 'dangerouslySetInnerHTML' ) { 51 | // Looking for dangerouslySetInnerHTML only. 52 | return; 53 | } 54 | 55 | $nextToken = $this->phpcsFile->findNext( Tokens::$emptyTokens, $stackPtr + 1, null, true, null, true ); 56 | 57 | if ( $this->tokens[ $nextToken ]['code'] !== T_EQUAL ) { 58 | // Not an assignment. 59 | return; 60 | } 61 | 62 | $nextNextToken = $this->phpcsFile->findNext( Tokens::$emptyTokens, $nextToken + 1, null, true, null, true ); 63 | 64 | if ( $this->tokens[ $nextNextToken ]['code'] !== T_OBJECT ) { 65 | // Not react syntax. 66 | return; 67 | } 68 | 69 | $message = "Any HTML passed to `%s` gets executed. Please make sure it's properly escaped."; 70 | $data = [ $this->tokens[ $stackPtr ]['content'] ]; 71 | $this->phpcsFile->addError( $message, $stackPtr, 'Found', $data ); 72 | } 73 | 74 | /** 75 | * Provide the version number in which the sniff was deprecated. 76 | * 77 | * @return string 78 | */ 79 | public function getDeprecationVersion() { 80 | return 'VIP-Coding-Standard v3.1.0'; 81 | } 82 | 83 | /** 84 | * Provide the version number in which the sniff will be removed. 85 | * 86 | * @return string 87 | */ 88 | public function getRemovalVersion() { 89 | return 'VIP-Coding-Standard v4.0.0'; 90 | } 91 | 92 | /** 93 | * Provide a custom message to display with the deprecation. 94 | * 95 | * @return string 96 | */ 97 | public function getDeprecationMessage() { 98 | return 'Support for scanning JavaScript files will be removed from PHP_CodeSniffer, so this sniff is no longer viable.'; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /WordPressVIPMinimum/Sniffs/Security/MustacheSniff.php: -------------------------------------------------------------------------------- 1 | 31 | */ 32 | public function register() { 33 | $targets = Tokens::$textStringTokens; 34 | $targets[ T_STRING ] = T_STRING; 35 | 36 | return $targets; 37 | } 38 | 39 | /** 40 | * Processes this test, when one of its tokens is encountered. 41 | * 42 | * @param int $stackPtr The position of the current token in the stack passed in $tokens. 43 | * 44 | * @return void 45 | */ 46 | public function process_token( $stackPtr ) { 47 | 48 | if ( strpos( $this->tokens[ $stackPtr ]['content'], '{{{' ) !== false && strpos( $this->tokens[ $stackPtr ]['content'], '}}}' ) !== false ) { 49 | // Mustache unescaped output notation. 50 | $message = 'Found Mustache unescaped output notation: "{{{}}}".'; 51 | $this->phpcsFile->addWarning( $message, $stackPtr, 'OutputNotation' ); 52 | } 53 | 54 | if ( strpos( $this->tokens[ $stackPtr ]['content'], '{{&' ) !== false ) { 55 | // Mustache unescaped variable notation. 56 | $message = 'Found Mustache unescape variable notation: "{{&".'; 57 | $this->phpcsFile->addWarning( $message, $stackPtr, 'VariableNotation' ); 58 | } 59 | 60 | $start_delimiter_change = strpos( $this->tokens[ $stackPtr ]['content'], '{{=' ); 61 | if ( $start_delimiter_change !== false ) { 62 | // Mustache delimiter change. 63 | $end_delimiter_change = strpos( $this->tokens[ $stackPtr ]['content'], '=}}' ); 64 | if ( $end_delimiter_change !== false && $start_delimiter_change < $end_delimiter_change ) { 65 | $start_new_delimiter = $start_delimiter_change + 3; 66 | $new_delimiter_length = $end_delimiter_change - ( $start_delimiter_change + 3 ); 67 | $new_delimiter = substr( $this->tokens[ $stackPtr ]['content'], $start_new_delimiter, $new_delimiter_length ); 68 | 69 | $message = 'Found Mustache delimiter change notation. New delimiter is: %s.'; 70 | $data = [ $new_delimiter ]; 71 | $this->phpcsFile->addWarning( $message, $stackPtr, 'DelimiterChange', $data ); 72 | } 73 | } 74 | 75 | if ( strpos( $this->tokens[ $stackPtr ]['content'], '.SafeString' ) !== false ) { 76 | // Handlebars.js Handlebars.SafeString does not get escaped. 77 | $message = 'Found Handlebars.SafeString call which does not get escaped.'; 78 | $this->phpcsFile->addWarning( $message, $stackPtr, 'SafeString' ); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /WordPressVIPMinimum/Sniffs/JS/StrippingTagsSniff.php: -------------------------------------------------------------------------------- 1 | 34 | */ 35 | public function register() { 36 | return [ 37 | T_STRING, 38 | ]; 39 | } 40 | 41 | 42 | /** 43 | * Processes this test, when one of its tokens is encountered. 44 | * 45 | * @param int $stackPtr The position of the current token in the stack passed in $tokens. 46 | * 47 | * @return void 48 | */ 49 | public function process_token( $stackPtr ) { 50 | 51 | if ( $this->tokens[ $stackPtr ]['content'] !== 'html' ) { 52 | // Looking for html() only. 53 | return; 54 | } 55 | 56 | $nextToken = $this->phpcsFile->findNext( Tokens::$emptyTokens, $stackPtr + 1, null, true, null, true ); 57 | 58 | if ( $this->tokens[ $nextToken ]['code'] !== T_OPEN_PARENTHESIS ) { 59 | // Not a function. 60 | return; 61 | } 62 | 63 | $afterFunctionCall = $this->phpcsFile->findNext( Tokens::$emptyTokens, $this->tokens[ $nextToken ]['parenthesis_closer'] + 1, null, true, null, true ); 64 | 65 | if ( $this->tokens[ $afterFunctionCall ]['code'] !== T_OBJECT_OPERATOR ) { 66 | return; 67 | } 68 | 69 | $nextToken = $this->phpcsFile->findNext( Tokens::$emptyTokens, $afterFunctionCall + 1, null, true, null, true ); 70 | 71 | if ( $this->tokens[ $nextToken ]['code'] === T_STRING && $this->tokens[ $nextToken ]['content'] === 'text' ) { 72 | $message = 'Vulnerable tag stripping approach detected.'; 73 | $this->phpcsFile->addError( $message, $stackPtr, 'VulnerableTagStripping' ); 74 | } 75 | } 76 | 77 | /** 78 | * Provide the version number in which the sniff was deprecated. 79 | * 80 | * @return string 81 | */ 82 | public function getDeprecationVersion() { 83 | return 'VIP-Coding-Standard v3.1.0'; 84 | } 85 | 86 | /** 87 | * Provide the version number in which the sniff will be removed. 88 | * 89 | * @return string 90 | */ 91 | public function getRemovalVersion() { 92 | return 'VIP-Coding-Standard v4.0.0'; 93 | } 94 | 95 | /** 96 | * Provide a custom message to display with the deprecation. 97 | * 98 | * @return string 99 | */ 100 | public function getDeprecationMessage() { 101 | return 'Support for scanning JavaScript files will be removed from PHP_CodeSniffer, so this sniff is no longer viable.'; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /WordPressVIPMinimum/Sniffs/Performance/WPQueryParamsSniff.php: -------------------------------------------------------------------------------- 1 | >> 33 | */ 34 | public function getGroups() { 35 | return [ 36 | // WordPress.com: https://lobby.vip.wordpress.com/wordpress-com-documentation/uncached-functions/. 37 | // VIP Go: https://wpvip.com/documentation/vip-go/uncached-functions/. 38 | 'SuppressFilters' => [ 39 | 'name' => 'SuppressFilters', 40 | 'type' => 'error', 41 | 'message' => 'Setting `suppress_filters` to `true` is prohibited.', 42 | 'keys' => [ 43 | 'suppress_filters', 44 | ], 45 | ], 46 | 'PostNotIn' => [ 47 | 'name' => 'PostNotIn', 48 | 'type' => 'warning', 49 | 'message' => 'Using exclusionary parameters, like %s, in calls to get_posts() should be done with caution, see https://docs.wpvip.com/databases/optimize-queries/using-post__not_in/ for more information.', 50 | 'keys' => [ 51 | 'post__not_in', 52 | 'exclude', 53 | ], 54 | ], 55 | ]; 56 | } 57 | 58 | /** 59 | * Processes this test, when one of its tokens is encountered. 60 | * 61 | * @param int $stackPtr The position of the current token in the stack. 62 | * 63 | * @return void 64 | */ 65 | public function process_token( $stackPtr ) { 66 | $this->in_get_users = ContextHelper::is_in_function_call( $this->phpcsFile, $stackPtr, [ 'get_users' => true ], true, true ); 67 | 68 | parent::process_token( $stackPtr ); 69 | } 70 | 71 | 72 | /** 73 | * Callback to process a confirmed key which doesn't need custom logic, but should always error. 74 | * 75 | * @param string $key Array index / key. 76 | * @param mixed $val Assigned value. 77 | * @param int $line Token line. 78 | * @param array $group Group definition. 79 | * 80 | * @return bool FALSE if no match, TRUE if matches. 81 | */ 82 | public function callback( $key, $val, $line, $group ) { 83 | switch ( $group['name'] ) { 84 | case 'SuppressFilters': 85 | return ( $val === 'true' ); 86 | 87 | case 'PostNotIn': 88 | if ( $key === 'exclude' && $this->in_get_users !== false ) { 89 | // This is not an array used by get_posts(). See #672. 90 | return false; 91 | } 92 | 93 | return true; 94 | 95 | default: 96 | return false; 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /WordPressVIPMinimum/Sniffs/Files/IncludingNonPHPFileSniff.php: -------------------------------------------------------------------------------- 1 | Key is the extension, value is irrelevant. 29 | */ 30 | private $php_extensions = [ 31 | 'php' => true, 32 | 'inc' => true, 33 | 'phar' => true, 34 | ]; 35 | 36 | /** 37 | * File extensions used for SVG and CSS files. 38 | * 39 | * @var array Key is the extension, value is irrelevant. 40 | */ 41 | private $svg_css_extensions = [ 42 | 'css' => true, 43 | 'svg' => true, 44 | ]; 45 | 46 | /** 47 | * Returns an array of tokens this test wants to listen for. 48 | * 49 | * @return array 50 | */ 51 | public function register() { 52 | return Tokens::$includeTokens; 53 | } 54 | 55 | /** 56 | * Processes this test, when one of its tokens is encountered. 57 | * 58 | * @param int $stackPtr The position of the current token in the stack passed in $tokens. 59 | * 60 | * @return void 61 | */ 62 | public function process_token( $stackPtr ) { 63 | $end_of_statement = BCFile::findEndOfStatement( $this->phpcsFile, $stackPtr ); 64 | $curStackPtr = ( $end_of_statement + 1 ); 65 | 66 | do { 67 | $curStackPtr = $this->phpcsFile->findPrevious( Tokens::$stringTokens, $curStackPtr - 1, $stackPtr ); 68 | if ( $curStackPtr === false ) { 69 | return; 70 | } 71 | 72 | $stringWithoutEnclosingQuotationMarks = trim( $this->tokens[ $curStackPtr ]['content'], "\"'" ); 73 | 74 | $isFileName = preg_match( '`\.([a-z]{2,})$`i', $stringWithoutEnclosingQuotationMarks, $regexMatches ); 75 | 76 | if ( $isFileName !== 1 ) { 77 | continue; 78 | } 79 | 80 | $extension = strtolower( $regexMatches[1] ); 81 | if ( isset( $this->php_extensions[ $extension ] ) === true ) { 82 | return; 83 | } 84 | 85 | $message = 'Local non-PHP file should be loaded via `file_get_contents` rather than via `%s`. Found: %s'; 86 | $data = [ 87 | strtolower( $this->tokens[ $stackPtr ]['content'] ), 88 | $this->tokens[ $curStackPtr ]['content'], 89 | ]; 90 | $code = 'IncludingNonPHPFile'; 91 | 92 | if ( isset( $this->svg_css_extensions[ $extension ] ) === true ) { 93 | // Be more specific for SVG and CSS files. 94 | $message = 'Local SVG and CSS files should be loaded via `file_get_contents` rather than via `%s`. Found: %s'; 95 | $code = 'IncludingSVGCSSFile'; 96 | } 97 | 98 | $this->phpcsFile->addError( $message, $curStackPtr, $code, $data ); 99 | 100 | // Don't throw more than one error for any one statement. 101 | return; 102 | 103 | } while ( $curStackPtr > $stackPtr ); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /WordPressVIPMinimum/Sniffs/JS/InnerHTMLSniff.php: -------------------------------------------------------------------------------- 1 | 34 | */ 35 | public function register() { 36 | return [ 37 | T_STRING, 38 | ]; 39 | } 40 | 41 | /** 42 | * Processes this test, when one of its tokens is encountered. 43 | * 44 | * @param int $stackPtr The position of the current token in the stack passed in $tokens. 45 | * 46 | * @return void 47 | */ 48 | public function process_token( $stackPtr ) { 49 | 50 | if ( $this->tokens[ $stackPtr ]['content'] !== 'innerHTML' ) { 51 | // Looking for .innerHTML only. 52 | return; 53 | } 54 | 55 | $prevToken = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, $stackPtr - 1, null, true, null, true ); 56 | 57 | if ( $this->tokens[ $prevToken ]['code'] !== T_OBJECT_OPERATOR ) { 58 | return; 59 | } 60 | 61 | $nextToken = $this->phpcsFile->findNext( Tokens::$emptyTokens, $stackPtr + 1, null, true, null, true ); 62 | 63 | if ( $this->tokens[ $nextToken ]['code'] !== T_EQUAL ) { 64 | // Not an assignment. 65 | return; 66 | } 67 | 68 | $nextToken = $this->phpcsFile->findNext( Tokens::$emptyTokens, $nextToken + 1, null, true, null, true ); 69 | $foundVariable = false; 70 | 71 | while ( $nextToken !== false && $this->tokens[ $nextToken ]['code'] !== T_SEMICOLON ) { 72 | 73 | if ( $this->tokens[ $nextToken ]['code'] === T_STRING ) { 74 | $foundVariable = true; 75 | break; 76 | } 77 | 78 | $nextToken = $this->phpcsFile->findNext( Tokens::$emptyTokens, $nextToken + 1, null, true, null, true ); 79 | } 80 | 81 | if ( $foundVariable === true ) { 82 | $message = 'Any HTML passed to `%s` gets executed. Consider using `.textContent` or make sure that used variables are properly escaped.'; 83 | $data = [ $this->tokens[ $stackPtr ]['content'] ]; 84 | $this->phpcsFile->addWarning( $message, $stackPtr, 'Found', $data ); 85 | } 86 | } 87 | 88 | /** 89 | * Provide the version number in which the sniff was deprecated. 90 | * 91 | * @return string 92 | */ 93 | public function getDeprecationVersion() { 94 | return 'VIP-Coding-Standard v3.1.0'; 95 | } 96 | 97 | /** 98 | * Provide the version number in which the sniff will be removed. 99 | * 100 | * @return string 101 | */ 102 | public function getRemovalVersion() { 103 | return 'VIP-Coding-Standard v4.0.0'; 104 | } 105 | 106 | /** 107 | * Provide a custom message to display with the deprecation. 108 | * 109 | * @return string 110 | */ 111 | public function getDeprecationMessage() { 112 | return 'Support for scanning JavaScript files will be removed from PHP_CodeSniffer, so this sniff is no longer viable.'; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /WordPressVIPMinimum/Sniffs/JS/StringConcatSniff.php: -------------------------------------------------------------------------------- 1 | 34 | */ 35 | public function register() { 36 | return [ 37 | T_PLUS, 38 | ]; 39 | } 40 | 41 | 42 | /** 43 | * Processes this test, when one of its tokens is encountered. 44 | * 45 | * @param int $stackPtr The position of the current token in the stack passed in $tokens. 46 | * 47 | * @return void 48 | */ 49 | public function process_token( $stackPtr ) { 50 | 51 | $nextToken = $this->phpcsFile->findNext( Tokens::$emptyTokens, $stackPtr + 1, null, true, null, true ); 52 | 53 | if ( $this->tokens[ $nextToken ]['code'] === T_CONSTANT_ENCAPSED_STRING && strpos( $this->tokens[ $nextToken ]['content'], '<' ) !== false && preg_match( '/\<\/[a-zA-Z]+/', $this->tokens[ $nextToken ]['content'] ) === 1 ) { 54 | $data = [ '+' . $this->tokens[ $nextToken ]['content'] ]; 55 | $this->addFoundError( $stackPtr, $data ); 56 | } 57 | 58 | $prevToken = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, $stackPtr - 1, null, true, null, true ); 59 | 60 | if ( $this->tokens[ $prevToken ]['code'] === T_CONSTANT_ENCAPSED_STRING && strpos( $this->tokens[ $prevToken ]['content'], '<' ) !== false && preg_match( '/\<[a-zA-Z]+/', $this->tokens[ $prevToken ]['content'] ) === 1 ) { 61 | $data = [ $this->tokens[ $nextToken ]['content'] . '+' ]; 62 | $this->addFoundError( $stackPtr, $data ); 63 | } 64 | } 65 | 66 | /** 67 | * Consolidated violation. 68 | * 69 | * @param int $stackPtr The position of the current token in the stack passed in $tokens. 70 | * @param array $data Replacements for the error message. 71 | * 72 | * @return void 73 | */ 74 | private function addFoundError( $stackPtr, array $data ) { 75 | $message = 'HTML string concatenation detected, this is a security risk, use DOM node construction or a templating language instead: %s.'; 76 | $this->phpcsFile->addError( $message, $stackPtr, 'Found', $data ); 77 | } 78 | 79 | /** 80 | * Provide the version number in which the sniff was deprecated. 81 | * 82 | * @return string 83 | */ 84 | public function getDeprecationVersion() { 85 | return 'VIP-Coding-Standard v3.1.0'; 86 | } 87 | 88 | /** 89 | * Provide the version number in which the sniff will be removed. 90 | * 91 | * @return string 92 | */ 93 | public function getRemovalVersion() { 94 | return 'VIP-Coding-Standard v4.0.0'; 95 | } 96 | 97 | /** 98 | * Provide a custom message to display with the deprecation. 99 | * 100 | * @return string 101 | */ 102 | public function getDeprecationMessage() { 103 | return 'Support for scanning JavaScript files will be removed from PHP_CodeSniffer, so this sniff is no longer viable.'; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /WordPressVIPMinimum/Sniffs/Functions/StripTagsSniff.php: -------------------------------------------------------------------------------- 1 | Key is the function name, value irrelevant. 33 | */ 34 | protected $target_functions = [ 35 | 'strip_tags' => true, 36 | ]; 37 | 38 | /** 39 | * Process the parameters of a matched function. 40 | * 41 | * @param int $stackPtr The position of the current token in the stack. 42 | * @param string $group_name The name of the group which was matched. 43 | * @param string $matched_content The token content (function name) which was matched 44 | * in lowercase. 45 | * @param array $parameters Array with information about the parameters. 46 | * 47 | * @return void 48 | */ 49 | public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) { 50 | $string_param = PassedParameters::getParameterFromStack( $parameters, 1, 'string' ); 51 | $allowed_tags_param = PassedParameters::getParameterFromStack( $parameters, 2, 'allowed_tags' ); 52 | 53 | if ( $string_param !== false && $allowed_tags_param === false ) { 54 | $this->add_warning( $stackPtr, 'StripTagsOneParameter' ); 55 | } elseif ( $allowed_tags_param !== false ) { 56 | $message = '`strip_tags()` does not strip CSS and JS in between the script and style tags. Use `wp_kses()` instead to allow only the HTML you need.'; 57 | $this->phpcsFile->addWarning( $message, $stackPtr, 'StripTagsTwoParameters' ); 58 | } else { 59 | $this->add_warning( $stackPtr ); 60 | } 61 | } 62 | 63 | /** 64 | * Process the function if no parameters were found. 65 | * 66 | * @param int $stackPtr The position of the current token in the stack. 67 | * @param string $group_name The name of the group which was matched. 68 | * @param string $matched_content The token content (function name) which was matched 69 | * in lowercase. 70 | * 71 | * @return void 72 | */ 73 | public function process_no_parameters( $stackPtr, $group_name, $matched_content ) { 74 | $this->add_warning( $stackPtr ); 75 | } 76 | 77 | /** 78 | * Process the function if it is used as a first class callable. 79 | * 80 | * @param int $stackPtr The position of the current token in the stack. 81 | * @param string $group_name The name of the group which was matched. 82 | * @param string $matched_content The token content (function name) which was matched 83 | * in lowercase. 84 | * 85 | * @return void 86 | */ 87 | public function process_first_class_callable( $stackPtr, $group_name, $matched_content ) { 88 | $this->add_warning( $stackPtr ); 89 | } 90 | 91 | /** 92 | * Add a warning if the function is used at all. 93 | * 94 | * @param int $stackPtr The position of the current token in the stack. 95 | * @param string $error_code Error code to use for the warning. 96 | * 97 | * @return void 98 | */ 99 | private function add_warning( $stackPtr, $error_code = 'Used' ) { 100 | $message = '`strip_tags()` does not strip CSS and JS in between the script and style tags. Use `wp_strip_all_tags()` to strip all tags.'; 101 | $this->phpcsFile->addWarning( $message, $stackPtr, $error_code ); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /WordPressVIPMinimum/Sniffs/Performance/FetchingRemoteDataSniff.php: -------------------------------------------------------------------------------- 1 | Key is the function name, value irrelevant. 32 | */ 33 | protected $target_functions = [ 34 | 'file_get_contents' => true, 35 | ]; 36 | 37 | /** 38 | * Process the parameters of a matched function. 39 | * 40 | * @param int $stackPtr The position of the current token in the stack. 41 | * @param string $group_name The name of the group which was matched. 42 | * @param string $matched_content The token content (function name) which was matched 43 | * in lowercase. 44 | * @param array $parameters Array with information about the parameters. 45 | * 46 | * @return void 47 | */ 48 | public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) { 49 | $filename_param = PassedParameters::getParameterFromStack( $parameters, 1, 'filename' ); 50 | if ( $filename_param === false ) { 51 | // Missing required parameter. Probably live coding, nothing to examine (yet). Bow out. 52 | return; 53 | } 54 | 55 | $data = [ $matched_content ]; 56 | $param_start = $filename_param['start']; 57 | $search_end = ( $filename_param['end'] + 1 ); 58 | 59 | $has_magic_dir = $this->phpcsFile->findNext( T_DIR, $param_start, $search_end ); 60 | if ( $has_magic_dir !== false ) { 61 | // In all likelyhood a local file (disregarding creative code). 62 | return; 63 | } 64 | 65 | $isRemoteFile = false; 66 | $search_start = $param_start; 67 | // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition -- Valid usage. 68 | while ( ( $has_text_string = $this->phpcsFile->findNext( Tokens::$stringTokens, $search_start, $search_end ) ) !== false ) { 69 | if ( strpos( $this->tokens[ $has_text_string ]['content'], '://' ) !== false ) { 70 | $isRemoteFile = true; 71 | break; 72 | } 73 | 74 | $search_start = ( $has_text_string + 1 ); 75 | } 76 | 77 | if ( $isRemoteFile === true ) { 78 | $message = '`%s()` is highly discouraged for remote requests, please use `wpcom_vip_file_get_contents()` or `vip_safe_wp_remote_get()` instead.'; 79 | $this->phpcsFile->addWarning( $message, $stackPtr, 'FileGetContentsRemoteFile', $data ); 80 | return; 81 | } 82 | 83 | /* 84 | * Okay, so we haven't been able to determine for certain this is a remote file. 85 | * Check for tokens which would make the parameter contents dynamic. 86 | */ 87 | $ignore = Tokens::$emptyTokens; 88 | $ignore += Tokens::$stringTokens; 89 | $ignore += [ T_STRING_CONCAT => T_STRING_CONCAT ]; 90 | 91 | $has_non_text_string = $this->phpcsFile->findNext( $ignore, $param_start, $search_end, true ); 92 | if ( $has_non_text_string !== false ) { 93 | $this->add_contents_unknown_warning( $stackPtr, $data ); 94 | } 95 | } 96 | 97 | /** 98 | * Process the function if it is used as a first class callable. 99 | * 100 | * @param int $stackPtr The position of the current token in the stack. 101 | * @param string $group_name The name of the group which was matched. 102 | * @param string $matched_content The token content (function name) which was matched 103 | * in lowercase. 104 | * 105 | * @return void 106 | */ 107 | public function process_first_class_callable( $stackPtr, $group_name, $matched_content ) { 108 | $this->add_contents_unknown_warning( $stackPtr, [ $matched_content ] ); 109 | } 110 | 111 | /** 112 | * Add a warning if the function is used with unknown parameter(s) or with a $filename parameter for which 113 | * it could not be determined if it references a local file or a remote file. 114 | * 115 | * @param int $stackPtr The position of the current token in the stack. 116 | * @param array $data Data to use for string replacement in the error message. 117 | * 118 | * @return void 119 | */ 120 | private function add_contents_unknown_warning( $stackPtr, $data ) { 121 | $message = '`%s()` is highly discouraged for remote requests, please use `wpcom_vip_file_get_contents()` or `vip_safe_wp_remote_get()` instead. If it\'s for a local file please use WP_Filesystem instead.'; 122 | $this->phpcsFile->addWarning( $message, $stackPtr, 'FileGetContentsUnknown', $data ); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /WordPressVIPMinimum/Sniffs/Security/PHPFilterFunctionsSniff.php: -------------------------------------------------------------------------------- 1 | Key is the function name. 38 | */ 39 | protected $target_functions = [ 40 | 'filter_var' => [ 41 | 'param_position' => 2, 42 | 'param_name' => 'filter', 43 | ], 44 | 'filter_input' => [ 45 | 'param_position' => 3, 46 | 'param_name' => 'filter', 47 | ], 48 | 'filter_var_array' => [ 49 | 'param_position' => 2, 50 | 'param_name' => 'options', 51 | ], 52 | 'filter_input_array' => [ 53 | 'param_position' => 2, 54 | 'param_name' => 'options', 55 | ], 56 | ]; 57 | 58 | /** 59 | * List of restricted filter names. 60 | * 61 | * @var array 62 | */ 63 | private $restricted_filters = [ 64 | 'FILTER_DEFAULT' => true, 65 | 'FILTER_UNSAFE_RAW' => true, 66 | ]; 67 | 68 | /** 69 | * Process the parameters of a matched function. 70 | * 71 | * @param int $stackPtr The position of the current token in the stack. 72 | * @param string $group_name The name of the group which was matched. 73 | * @param string $matched_content The token content (function name) which was matched 74 | * in lowercase. 75 | * @param array $parameters Array with information about the parameters. 76 | * 77 | * @return int|void Integer stack pointer to skip forward or void to continue 78 | * normal file processing. 79 | */ 80 | public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) { 81 | $param_position = $this->target_functions[ $matched_content ]['param_position']; 82 | $param_name = $this->target_functions[ $matched_content ]['param_name']; 83 | 84 | $target_param = PassedParameters::getParameterFromStack( $parameters, $param_position, $param_name ); 85 | if ( $target_param === false ) { 86 | /* 87 | * Check for PHP 5.6+ argument unpacking. 88 | * 89 | * No need for extensive defensive coding, we already know this is syntactically a valid function call, 90 | * otherwise this method would not have been reached. 91 | */ 92 | $tokens = $this->phpcsFile->getTokens(); 93 | $open_parens = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true ); 94 | $has_ellipses = $this->phpcsFile->findNext( T_ELLIPSIS, ( $open_parens + 1 ), $tokens[ $open_parens ]['parenthesis_closer'] ); 95 | 96 | if ( $has_ellipses !== false ) { 97 | $target_nesting_level = 1; 98 | if ( isset( $tokens[ $open_parens ]['nested_parenthesis'] ) ) { 99 | $target_nesting_level = ( count( $tokens[ $open_parens ]['nested_parenthesis'] ) + 1 ); 100 | } 101 | 102 | if ( $target_nesting_level === count( $tokens[ $has_ellipses ]['nested_parenthesis'] ) ) { 103 | // Bow out as undetermined. 104 | return; 105 | } 106 | } 107 | 108 | $message = 'Missing $%s parameter for "%s()".'; 109 | $data = [ $param_name, $matched_content ]; 110 | 111 | // Error codes should probably be made more descriptive, but that would be a BC-break. 112 | $error_code = 'MissingSecondParameter'; 113 | if ( $param_position === 3 ) { 114 | $error_code = 'MissingThirdParameter'; 115 | } 116 | 117 | $this->phpcsFile->addWarning( $message, $stackPtr, $error_code, $data ); 118 | return; 119 | } 120 | 121 | if ( isset( $this->restricted_filters[ $target_param['clean'] ] ) ) { 122 | $first_non_empty = $this->phpcsFile->findNext( Tokens::$emptyTokens, $target_param['start'], ( $target_param['end'] + 1 ), true ); 123 | 124 | $message = 'Please use an appropriate filter to sanitize, as "%s" does no filtering, see: http://php.net/manual/en/filter.filters.sanitize.php.'; 125 | $data = [ $target_param['clean'] ]; 126 | $this->phpcsFile->addWarning( $message, $first_non_empty, 'RestrictedFilter', $data ); 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /WordPressVIPMinimum/Sniffs/Hooks/RestrictedHooksSniff.php: -------------------------------------------------------------------------------- 1 | Key is the function name, value irrelevant. 36 | */ 37 | protected $target_functions = [ 38 | 'add_filter' => true, 39 | 'add_action' => true, 40 | ]; 41 | 42 | /** 43 | * List of restricted filters by groups. 44 | * 45 | * @var array>> 46 | */ 47 | private $restricted_hook_groups = [ 48 | 'upload_mimes' => [ 49 | // TODO: This error message needs a link to the VIP Documentation, see https://github.com/Automattic/VIP-Coding-Standards/issues/235. 50 | 'type' => 'warning', 51 | 'msg' => 'Please ensure that the mimes being filtered do not include insecure types (i.e. SVG, SWF, etc.). Manual inspection required.', 52 | 'hooks' => [ 53 | 'upload_mimes', 54 | ], 55 | ], 56 | 'http_request' => [ 57 | // https://docs.wpvip.com/technical-references/code-quality-and-best-practices/retrieving-remote-data/. 58 | 'type' => 'warning', 59 | 'msg' => 'Please ensure that the timeout being filtered is not greater than 3s since remote requests require the user to wait for completion before the rest of the page will load. Manual inspection required.', 60 | 'hooks' => [ 61 | 'http_request_timeout', 62 | 'http_request_args', 63 | ], 64 | ], 65 | 'robotstxt' => [ 66 | // https://docs.wpvip.com/how-tos/modify-the-robots-txt-file/. 67 | 'type' => 'warning', 68 | 'msg' => 'Don\'t forget to flush the robots.txt cache by going to Settings > Reading and toggling the privacy settings.', 69 | 'hooks' => [ 70 | 'do_robotstxt', 71 | 'robots_txt', 72 | ], 73 | ], 74 | ]; 75 | 76 | /** 77 | * Process the parameters of a matched function. 78 | * 79 | * @param int $stackPtr The position of the current token in the stack. 80 | * @param string $group_name The name of the group which was matched. 81 | * @param string $matched_content The token content (function name) which was matched 82 | * in lowercase. 83 | * @param array $parameters Array with information about the parameters. 84 | * 85 | * @return int|void Integer stack pointer to skip forward or void to continue 86 | * normal file processing. 87 | */ 88 | public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) { 89 | $hook_name_param = PassedParameters::getParameterFromStack( $parameters, 1, 'hook_name' ); 90 | if ( $hook_name_param === false ) { 91 | // Missing required parameter. Nothing to examine. Bow out. 92 | return; 93 | } 94 | 95 | $normalized_hook_name = $this->normalize_hook_name_from_parameter( $hook_name_param ); 96 | if ( $normalized_hook_name === '' ) { 97 | // Dynamic hook name. Cannot reliably determine if it's one of the targets. Bow out. 98 | return; 99 | } 100 | 101 | foreach ( $this->restricted_hook_groups as $group => $group_args ) { 102 | foreach ( $group_args['hooks'] as $hook ) { 103 | if ( $normalized_hook_name === $hook ) { 104 | $isError = ( $group_args['type'] === 'error' ); 105 | MessageHelper::addMessage( $this->phpcsFile, $group_args['msg'], $stackPtr, $isError, $hook ); 106 | } 107 | } 108 | } 109 | } 110 | 111 | /** 112 | * Normalize hook name parameter. 113 | * 114 | * @param array $parameter Array with information about a parameter. 115 | * 116 | * @return string Normalized hook name or an empty string if the hook name could not be determined. 117 | */ 118 | private function normalize_hook_name_from_parameter( $parameter ) { 119 | $allowed_tokens = Tokens::$emptyTokens; 120 | $allowed_tokens += [ 121 | T_STRING_CONCAT => T_STRING_CONCAT, 122 | T_CONSTANT_ENCAPSED_STRING => T_CONSTANT_ENCAPSED_STRING, 123 | ]; 124 | 125 | $has_disallowed_token = $this->phpcsFile->findNext( $allowed_tokens, $parameter['start'], ( $parameter['end'] + 1 ), true ); 126 | if ( $has_disallowed_token !== false ) { 127 | return ''; 128 | } 129 | 130 | $hook_name = ''; 131 | for ( $i = $parameter['start']; $i <= $parameter['end']; $i++ ) { 132 | if ( $this->tokens[ $i ]['code'] === T_CONSTANT_ENCAPSED_STRING ) { 133 | $hook_name .= TextStrings::stripQuotes( $this->tokens[ $i ]['content'] ); 134 | } 135 | } 136 | 137 | return $hook_name; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /WordPressVIPMinimum/Sniffs/Constants/RestrictedConstantsSniff.php: -------------------------------------------------------------------------------- 1 | 25 | */ 26 | public $restrictedConstantNames = [ 27 | 'A8C_PROXIED_REQUEST', 28 | ]; 29 | 30 | /** 31 | * List of restricted constant declarations. 32 | * 33 | * @var array 34 | */ 35 | public $restrictedConstantDeclaration = [ 36 | 'JETPACK_DEV_DEBUG', 37 | 'WP_CRON_CONTROL_SECRET', 38 | ]; 39 | 40 | /** 41 | * List of (global) constant names, which should not be referenced in userland code, nor (re-)declared. 42 | * 43 | * {@internal The `public` versions of these properties can't be removed until the next major, 44 | * though a decision is still needed whether they should be removed at all. 45 | * Also see: Automattic/VIP-Coding-Standards#234 for more context and discussion about this.} 46 | * 47 | * @var array Key is the constant name, value is irrelevant. 48 | */ 49 | private $restrictedConstants = []; 50 | 51 | /** 52 | * List of (global) constants, which should not be (re-)declared, but may be referenced. 53 | * 54 | * {@internal The `public` versions of these properties can't be removed until the next major, 55 | * though a decision is still needed whether they should be removed at all. 56 | * Also see: Automattic/VIP-Coding-Standards#234 for more context and discussion about this.} 57 | * 58 | * @var array Key is the constant name, value is irrelevant. 59 | */ 60 | private $restrictedRedeclaration = []; 61 | 62 | /** 63 | * Returns an array of tokens this test wants to listen for. 64 | * 65 | * @return array 66 | */ 67 | public function register() { 68 | // For now, set the `private` properties based on the values of the `public` properties. 69 | // This should be revisited when Automattic/VIP-Coding-Standards#234 gets actioned. 70 | $this->restrictedConstants = array_flip( $this->restrictedConstantNames ); 71 | $this->restrictedRedeclaration = array_flip( $this->restrictedConstantDeclaration ); 72 | 73 | return [ 74 | T_CONSTANT_ENCAPSED_STRING, 75 | T_STRING, 76 | ]; 77 | } 78 | 79 | /** 80 | * Process this test when one of its tokens is encountered 81 | * 82 | * @param int $stackPtr The position of the current token in the stack passed in $tokens. 83 | * 84 | * @return void 85 | */ 86 | public function process_token( $stackPtr ) { 87 | 88 | if ( $this->tokens[ $stackPtr ]['code'] === T_STRING ) { 89 | $constantName = $this->tokens[ $stackPtr ]['content']; 90 | } else { 91 | $constantName = TextStrings::stripQuotes( $this->tokens[ $stackPtr ]['content'] ); 92 | } 93 | 94 | if ( isset( $this->restrictedConstants[ $constantName ] ) === false 95 | && isset( $this->restrictedRedeclaration[ $constantName ] ) === false 96 | ) { 97 | // Not the constant we are looking for. 98 | return; 99 | } 100 | 101 | if ( $this->tokens[ $stackPtr ]['code'] === T_STRING && isset( $this->restrictedConstants[ $constantName ] ) === true ) { 102 | $message = 'Code is touching the `%s` constant. Make sure it\'s used appropriately.'; 103 | $data = [ $constantName ]; 104 | $this->phpcsFile->addWarning( $message, $stackPtr, 'UsingRestrictedConstant', $data ); 105 | return; 106 | } 107 | 108 | // Find the previous non-empty token. 109 | $openBracket = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, $stackPtr - 1, null, true, null, true ); 110 | 111 | if ( $this->tokens[ $openBracket ]['code'] !== T_OPEN_PARENTHESIS ) { 112 | // Not a function call. 113 | return; 114 | } 115 | 116 | if ( isset( $this->tokens[ $openBracket ]['parenthesis_closer'] ) === false ) { 117 | // Not a function call. 118 | return; 119 | } 120 | 121 | // Find the previous non-empty token. 122 | $search = Tokens::$emptyTokens; 123 | $search[] = T_BITWISE_AND; 124 | $previous = $this->phpcsFile->findPrevious( $search, $openBracket - 1, null, true ); 125 | if ( $this->tokens[ $previous ]['code'] === T_FUNCTION ) { 126 | // It's a function definition, not a function call. 127 | return; 128 | } 129 | 130 | if ( $this->tokens[ $previous ]['code'] === T_STRING ) { 131 | $data = [ $constantName ]; 132 | if ( $this->tokens[ $previous ]['content'] === 'define' ) { 133 | $message = 'The definition of `%s` constant is prohibited. Please use a different name.'; 134 | $this->phpcsFile->addError( $message, $previous, 'DefiningRestrictedConstant', $data ); 135 | } elseif ( isset( $this->restrictedConstants[ $constantName ] ) === true ) { 136 | $message = 'Code is touching the `%s` constant. Make sure it\'s used appropriately.'; 137 | $this->phpcsFile->addWarning( $message, $previous, 'UsingRestrictedConstant', $data ); 138 | } 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /WordPressVIPMinimum/Sniffs/Security/EscapingVoidReturnFunctionsSniff.php: -------------------------------------------------------------------------------- 1 | Keys are the target functions, 39 | * value, the name and position of the target parameter. 40 | */ 41 | protected $target_functions = [ 42 | 'esc_attr' => [ 43 | 'param_position' => 1, 44 | 'param_name' => 'text', 45 | ], 46 | 'esc_attr__' => [ 47 | 'param_position' => 1, 48 | 'param_name' => 'text', 49 | ], 50 | 'esc_attr_e' => [ 51 | 'param_position' => 1, 52 | 'param_name' => 'text', 53 | ], 54 | 'esc_attr_x' => [ 55 | 'param_position' => 1, 56 | 'param_name' => 'text', 57 | ], 58 | 'esc_html' => [ 59 | 'param_position' => 1, 60 | 'param_name' => 'text', 61 | ], 62 | 'esc_html__' => [ 63 | 'param_position' => 1, 64 | 'param_name' => 'text', 65 | ], 66 | 'esc_html_e' => [ 67 | 'param_position' => 1, 68 | 'param_name' => 'text', 69 | ], 70 | 'esc_html_x' => [ 71 | 'param_position' => 1, 72 | 'param_name' => 'text', 73 | ], 74 | 'esc_js' => [ 75 | 'param_position' => 1, 76 | 'param_name' => 'text', 77 | ], 78 | 'esc_textarea' => [ 79 | 'param_position' => 1, 80 | 'param_name' => 'text', 81 | ], 82 | 'esc_url' => [ 83 | 'param_position' => 1, 84 | 'param_name' => 'url', 85 | ], 86 | 'esc_url_raw' => [ 87 | 'param_position' => 1, 88 | 'param_name' => 'url', 89 | ], 90 | 'esc_xml' => [ 91 | 'param_position' => 1, 92 | 'param_name' => 'text', 93 | ], 94 | 'tag_escape' => [ 95 | 'param_position' => 1, 96 | 'param_name' => 'tag_name', 97 | ], 98 | 'wp_kses' => [ 99 | 'param_position' => 1, 100 | 'param_name' => 'content', 101 | ], 102 | 'wp_kses_data' => [ 103 | 'param_position' => 1, 104 | 'param_name' => 'data', 105 | ], 106 | 'wp_kses_one_attr' => [ 107 | 'param_position' => 1, 108 | 'param_name' => 'attr', 109 | ], 110 | 'wp_kses_post' => [ 111 | 'param_position' => 1, 112 | 'param_name' => 'data', 113 | ], 114 | ]; 115 | 116 | /** 117 | * Process the parameters of a matched function. 118 | * 119 | * @param int $stackPtr The position of the current token in the stack. 120 | * @param string $group_name The name of the group which was matched. 121 | * @param string $matched_content The token content (function name) which was matched 122 | * in lowercase. 123 | * @param array $parameters Array with information about the parameters. 124 | * 125 | * @return void 126 | */ 127 | public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) { 128 | $param_position = $this->target_functions[ $matched_content ]['param_position']; 129 | $param_name = $this->target_functions[ $matched_content ]['param_name']; 130 | 131 | $target_param = PassedParameters::getParameterFromStack( $parameters, $param_position, $param_name ); 132 | if ( $target_param === false ) { 133 | // Missing (required) target parameter. Probably live coding, nothing to examine (yet). Bow out. 134 | return; 135 | } 136 | 137 | $ignore = Tokens::$emptyTokens; 138 | $ignore[ T_NS_SEPARATOR ] = T_NS_SEPARATOR; 139 | 140 | $next_token = $this->phpcsFile->findNext( $ignore, $target_param['start'], ( $target_param['end'] + 1 ), true ); 141 | if ( $next_token === false || $this->tokens[ $next_token ]['code'] !== T_STRING ) { 142 | // Not what we are looking for. 143 | return; 144 | } 145 | 146 | $next_after = $this->phpcsFile->findNext( Tokens::$emptyTokens, $next_token + 1, ( $target_param['end'] + 1 ), true ); 147 | if ( $next_after === false || $this->tokens[ $next_after ]['code'] !== T_OPEN_PARENTHESIS ) { 148 | // Not a function call inside the escaping function. 149 | return; 150 | } 151 | 152 | if ( $this->is_printing_function( $this->tokens[ $next_token ]['content'] ) ) { 153 | $message = 'Attempting to escape `%s()` which is printing its output.'; 154 | $data = [ $this->tokens[ $next_token ]['content'] ]; 155 | $this->phpcsFile->addError( $message, $stackPtr, 'Found', $data ); 156 | return; 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /WordPressVIPMinimum/Sniffs/JS/WindowSniff.php: -------------------------------------------------------------------------------- 1 | 34 | */ 35 | public function register() { 36 | return [ 37 | T_STRING, 38 | ]; 39 | } 40 | 41 | /** 42 | * List of window properties that need to be flagged. 43 | * 44 | * @var array> 45 | */ 46 | private $windowProperties = [ 47 | 'location' => [ 48 | 'href' => true, 49 | 'protocol' => true, 50 | 'host' => true, 51 | 'hostname' => true, 52 | 'pathname' => true, 53 | 'search' => true, 54 | 'hash' => true, 55 | 'username' => true, 56 | 'port' => true, 57 | 'password' => true, 58 | ], 59 | 'name' => true, 60 | 'status' => true, 61 | ]; 62 | 63 | /** 64 | * Processes this test, when one of its tokens is encountered. 65 | * 66 | * @param int $stackPtr The position of the current token in the stack passed in $tokens. 67 | * 68 | * @return void 69 | */ 70 | public function process_token( $stackPtr ) { 71 | 72 | if ( $this->tokens[ $stackPtr ]['content'] !== 'window' ) { 73 | // Doesn't begin with 'window', bail. 74 | return; 75 | } 76 | 77 | $nextTokenPtr = $this->phpcsFile->findNext( Tokens::$emptyTokens, $stackPtr + 1, null, true, null, true ); 78 | $nextToken = $this->tokens[ $nextTokenPtr ]['code']; 79 | if ( $nextToken !== T_OBJECT_OPERATOR && $nextToken !== T_OPEN_SQUARE_BRACKET ) { 80 | // No . or [' next, bail. 81 | return; 82 | } 83 | 84 | $nextNextTokenPtr = $this->phpcsFile->findNext( Tokens::$emptyTokens, $nextTokenPtr + 1, null, true, null, true ); 85 | if ( $nextNextTokenPtr === false ) { 86 | // Something went wrong, bail. 87 | return; 88 | } 89 | 90 | $nextNextToken = str_replace( [ '"', "'" ], '', $this->tokens[ $nextNextTokenPtr ]['content'] ); 91 | if ( ! isset( $this->windowProperties[ $nextNextToken ] ) ) { 92 | // Not in $windowProperties, bail. 93 | return; 94 | } 95 | 96 | $nextNextNextTokenPtr = $this->phpcsFile->findNext( array_merge( [ T_CLOSE_SQUARE_BRACKET ], Tokens::$emptyTokens ), $nextNextTokenPtr + 1, null, true, null, true ); 97 | $nextNextNextToken = $this->tokens[ $nextNextNextTokenPtr ]['code']; 98 | 99 | $nextNextNextNextToken = false; 100 | if ( $nextNextNextToken === T_OBJECT_OPERATOR || $nextNextNextToken === T_OPEN_SQUARE_BRACKET ) { 101 | $nextNextNextNextTokenPtr = $this->phpcsFile->findNext( Tokens::$emptyTokens, $nextNextNextTokenPtr + 1, null, true, null, true ); 102 | if ( $nextNextNextNextTokenPtr === false ) { 103 | // Something went wrong, bail. 104 | return; 105 | } 106 | 107 | $nextNextNextNextToken = str_replace( [ '"', "'" ], '', $this->tokens[ $nextNextNextNextTokenPtr ]['content'] ); 108 | if ( ! isset( $this->windowProperties[ $nextNextToken ][ $nextNextNextNextToken ] ) ) { 109 | // Not in $windowProperties, bail. 110 | return; 111 | } 112 | } 113 | 114 | $windowProperty = 'window.'; 115 | $windowProperty .= $nextNextNextNextToken ? $nextNextToken . '.' . $nextNextNextNextToken : $nextNextToken; 116 | $data = [ $windowProperty ]; 117 | 118 | $prevTokenPtr = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, $stackPtr - 1, null, true, null, true ); 119 | 120 | if ( $this->tokens[ $prevTokenPtr ]['code'] === T_EQUAL ) { 121 | // Variable assignment. 122 | $message = 'Data from JS global "%s" may contain user-supplied values and should be checked.'; 123 | $this->phpcsFile->addWarning( $message, $stackPtr, 'VarAssignment', $data ); 124 | 125 | return; 126 | } 127 | 128 | $message = 'Data from JS global "%s" may contain user-supplied values and should be sanitized before output to prevent XSS.'; 129 | $this->phpcsFile->addError( $message, $stackPtr, $nextNextToken, $data ); 130 | } 131 | 132 | /** 133 | * Provide the version number in which the sniff was deprecated. 134 | * 135 | * @return string 136 | */ 137 | public function getDeprecationVersion() { 138 | return 'VIP-Coding-Standard v3.1.0'; 139 | } 140 | 141 | /** 142 | * Provide the version number in which the sniff will be removed. 143 | * 144 | * @return string 145 | */ 146 | public function getRemovalVersion() { 147 | return 'VIP-Coding-Standard v4.0.0'; 148 | } 149 | 150 | /** 151 | * Provide a custom message to display with the deprecation. 152 | * 153 | * @return string 154 | */ 155 | public function getDeprecationMessage() { 156 | return 'Support for scanning JavaScript files will be removed from PHP_CodeSniffer, so this sniff is no longer viable.'; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /WordPressVIPMinimum/Sniffs/Performance/TaxonomyMetaInOptionsSniff.php: -------------------------------------------------------------------------------- 1 | 27 | */ 28 | public $option_functions = [ 29 | 'get_option', 30 | 'add_option', 31 | 'update_option', 32 | 'delete_option', 33 | ]; 34 | 35 | /** 36 | * List of possible variable names holding term ID. 37 | * 38 | * @var array 39 | */ 40 | public $taxonomy_term_patterns = [ 41 | 'category_id', 42 | 'cat_id', 43 | 'cat', 44 | 'term_id', 45 | 'term', 46 | 'tag_id', 47 | 'tag', 48 | ]; 49 | 50 | /** 51 | * The group name for this group of functions. 52 | * 53 | * @var string 54 | */ 55 | protected $group_name = 'option_functions'; 56 | 57 | /** 58 | * Functions this sniff is looking for. 59 | * 60 | * @var array Keys are the target functions, value irrelevant. 61 | */ 62 | protected $target_functions = [ 63 | 'get_option' => true, 64 | 'add_option' => true, 65 | 'update_option' => true, 66 | 'delete_option' => true, 67 | ]; 68 | 69 | 70 | /** 71 | * Process the parameters of a matched function. 72 | * 73 | * @param int $stackPtr The position of the current token in the stack. 74 | * @param string $group_name The name of the group which was matched. 75 | * @param string $matched_content The token content (function name) which was matched 76 | * in lowercase. 77 | * @param array $parameters Array with information about the parameters. 78 | * 79 | * @return void 80 | */ 81 | public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) { 82 | $target_param = PassedParameters::getParameterFromStack( $parameters, 1, 'option' ); 83 | if ( $target_param === false ) { 84 | // Missing (required) target parameter. Probably live coding, nothing to examine (yet). Bow out. 85 | return; 86 | } 87 | 88 | $param_start = $target_param['start']; 89 | $param_end = ( $target_param['end'] + 1 ); // Add one to include the last token in the parameter in findNext searches. 90 | 91 | $param_ptr = $this->phpcsFile->findNext( Tokens::$emptyTokens, $param_start, $param_end, true ); 92 | 93 | if ( $this->tokens[ $param_ptr ]['code'] === T_DOUBLE_QUOTED_STRING ) { 94 | foreach ( $this->taxonomy_term_patterns as $taxonomy_term_pattern ) { 95 | if ( strpos( $this->tokens[ $param_ptr ]['content'], $taxonomy_term_pattern ) !== false ) { 96 | $this->addPossibleTermMetaInOptionsWarning( $stackPtr ); 97 | return; 98 | } 99 | } 100 | } elseif ( $this->tokens[ $param_ptr ]['code'] === T_CONSTANT_ENCAPSED_STRING ) { 101 | 102 | $string_concat = $this->phpcsFile->findNext( Tokens::$emptyTokens, $param_ptr + 1, null, true ); 103 | if ( $this->tokens[ $string_concat ]['code'] !== T_STRING_CONCAT ) { 104 | return; 105 | } 106 | 107 | $variable_name = $this->phpcsFile->findNext( Tokens::$emptyTokens, $string_concat + 1, null, true ); 108 | if ( $this->tokens[ $variable_name ]['code'] !== T_VARIABLE ) { 109 | return; 110 | } 111 | 112 | foreach ( $this->taxonomy_term_patterns as $taxonomy_term_pattern ) { 113 | if ( strpos( $this->tokens[ $variable_name ]['content'], $taxonomy_term_pattern ) !== false ) { 114 | $this->addPossibleTermMetaInOptionsWarning( $stackPtr ); 115 | return; 116 | } 117 | } 118 | 119 | $object_operator = $this->phpcsFile->findNext( Tokens::$emptyTokens, $variable_name + 1, null, true ); 120 | if ( $this->tokens[ $object_operator ]['code'] !== T_OBJECT_OPERATOR 121 | && $this->tokens[ $object_operator ]['code'] !== T_NULLSAFE_OBJECT_OPERATOR 122 | ) { 123 | return; 124 | } 125 | 126 | $object_property = $this->phpcsFile->findNext( Tokens::$emptyTokens, $object_operator + 1, null, true ); 127 | if ( $this->tokens[ $object_property ]['code'] !== T_STRING ) { 128 | return; 129 | } 130 | 131 | foreach ( $this->taxonomy_term_patterns as $taxonomy_term_pattern ) { 132 | if ( strpos( $this->tokens[ $object_property ]['content'], $taxonomy_term_pattern ) !== false ) { 133 | $this->addPossibleTermMetaInOptionsWarning( $stackPtr ); 134 | return; 135 | } 136 | } 137 | } 138 | } 139 | 140 | /** 141 | * Helper method for composing the Warning for all possible cases. 142 | * 143 | * @param int $stackPtr The position of the current token in the stack passed in $tokens. 144 | * 145 | * @return void 146 | */ 147 | public function addPossibleTermMetaInOptionsWarning( $stackPtr ) { 148 | $message = 'Possible detection of storing taxonomy term meta in options table. Needs manual inspection. All such data should be stored in term_meta.'; 149 | $this->phpcsFile->addWarning( $message, $stackPtr, 'PossibleTermMetaInOptions' ); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /WordPressVIPMinimum/Sniffs/Security/UnderscorejsSniff.php: -------------------------------------------------------------------------------- 1 | |$)`'; 29 | 30 | /** 31 | * Regex to match execute notations containing a print command 32 | * and retrieve a code snippet. 33 | * 34 | * @var string 35 | */ 36 | const UNESCAPED_PRINT_REGEX = '`<%\s*(?:print\s*\(.+?\)\s*;|__p\s*\+=.+?)\s*%>`'; 37 | 38 | /** 39 | * Regex to match the "interpolate" keyword when used to overrule the ERB-style delimiters. 40 | * 41 | * @var string 42 | */ 43 | const INTERPOLATE_KEYWORD_REGEX = '`(?:templateSettings\.interpolate|\.interpolate\s*=\s*/|interpolate\s*:\s*/)`'; 44 | 45 | /** 46 | * A list of tokenizers this sniff supports. 47 | * 48 | * @var string[] 49 | */ 50 | public $supportedTokenizers = [ 'JS', 'PHP' ]; 51 | 52 | /** 53 | * Returns an array of tokens this test wants to listen for. 54 | * 55 | * @return array 56 | */ 57 | public function register() { 58 | $targets = Tokens::$textStringTokens; 59 | $targets[] = T_PROPERTY; 60 | $targets[] = T_STRING; 61 | 62 | return $targets; 63 | } 64 | 65 | /** 66 | * Processes this test, when one of its tokens is encountered. 67 | * 68 | * @param int $stackPtr The position of the current token in the stack passed in $tokens. 69 | * 70 | * @return void 71 | */ 72 | public function process_token( $stackPtr ) { 73 | /* 74 | * Ignore Gruntfile.js files as they are configuration, not code. 75 | */ 76 | $file_name = FilePath::getName( $this->phpcsFile ); 77 | $file_name = strtolower( basename( $file_name ) ); 78 | 79 | if ( $file_name === 'gruntfile.js' ) { 80 | return; 81 | } 82 | 83 | /* 84 | * Check for delimiter change in JS files. 85 | */ 86 | if ( $this->tokens[ $stackPtr ]['code'] === T_STRING 87 | || $this->tokens[ $stackPtr ]['code'] === T_PROPERTY 88 | ) { 89 | if ( $this->phpcsFile->tokenizerType !== 'JS' ) { 90 | // These tokens are only relevant for JS files. 91 | return; 92 | } 93 | 94 | if ( $this->tokens[ $stackPtr ]['content'] !== 'interpolate' ) { 95 | return; 96 | } 97 | 98 | // Check the context to prevent false positives. 99 | if ( $this->tokens[ $stackPtr ]['code'] === T_STRING ) { 100 | $prev = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $stackPtr - 1 ), null, true ); 101 | if ( $prev === false || $this->tokens[ $prev ]['code'] !== T_OBJECT_OPERATOR ) { 102 | return; 103 | } 104 | 105 | $prevPrev = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $stackPtr - 1 ), null, true ); 106 | $next = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true ); 107 | if ( ( $prevPrev === false 108 | || $this->tokens[ $prevPrev ]['code'] !== T_STRING 109 | || $this->tokens[ $prevPrev ]['content'] !== 'templateSettings' ) 110 | && ( $next === false 111 | || $this->tokens[ $next ]['code'] !== T_EQUAL ) 112 | ) { 113 | return; 114 | } 115 | } 116 | 117 | // Underscore.js delimiter change. 118 | $message = 'Found Underscore.js delimiter change notation.'; 119 | $this->phpcsFile->addWarning( $message, $stackPtr, 'InterpolateFound' ); 120 | 121 | return; 122 | } 123 | 124 | $content = TextStrings::stripQuotes( $this->tokens[ $stackPtr ]['content'] ); 125 | 126 | $match_count = preg_match_all( self::UNESCAPED_INTERPOLATE_REGEX, $content, $matches ); 127 | if ( $match_count > 0 ) { 128 | foreach ( $matches[0] as $match ) { 129 | if ( strpos( $match, '_.escape(' ) !== false ) { 130 | continue; 131 | } 132 | 133 | // Underscore.js unescaped output. 134 | $message = 'Found Underscore.js unescaped output notation: "%s".'; 135 | $data = [ $match ]; 136 | $this->phpcsFile->addWarning( $message, $stackPtr, 'OutputNotation', $data ); 137 | } 138 | } 139 | 140 | $match_count = preg_match_all( self::UNESCAPED_PRINT_REGEX, $content, $matches ); 141 | if ( $match_count > 0 ) { 142 | foreach ( $matches[0] as $match ) { 143 | if ( strpos( $match, '_.escape(' ) !== false ) { 144 | continue; 145 | } 146 | 147 | // Underscore.js unescaped output. 148 | $message = 'Found Underscore.js unescaped print execution: "%s".'; 149 | $data = [ $match ]; 150 | $this->phpcsFile->addWarning( $message, $stackPtr, 'PrintExecution', $data ); 151 | } 152 | } 153 | 154 | if ( $this->phpcsFile->tokenizerType !== 'JS' 155 | && preg_match( self::INTERPOLATE_KEYWORD_REGEX, $content ) > 0 156 | ) { 157 | // Underscore.js delimiter change. 158 | $message = 'Found Underscore.js delimiter change notation.'; 159 | $this->phpcsFile->addWarning( $message, $stackPtr, 'InterpolateFound' ); 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /WordPressVIPMinimum/Sniffs/Performance/CacheValueOverrideSniff.php: -------------------------------------------------------------------------------- 1 | >> 26 | */ 27 | public function getGroups() { 28 | return [ 29 | 'wp_cache_get' => [ 30 | 'functions' => [ 'wp_cache_get' ], 31 | ], 32 | ]; 33 | } 34 | 35 | /** 36 | * Process a matched token. 37 | * 38 | * @param int $stackPtr The position of the current token in the stack. 39 | * @param string $group_name The name of the group which was matched. 40 | * @param string $matched_content The token content (function name) which was matched 41 | * in lowercase. 42 | * 43 | * @return void 44 | */ 45 | public function process_matched_token( $stackPtr, $group_name, $matched_content ) { 46 | $openBracket = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true ); 47 | if ( $openBracket === false || isset( $this->tokens[ $openBracket ]['parenthesis_closer'] ) === false ) { 48 | // Import use statement for function or parse error/live coding. Ignore. 49 | return; 50 | } 51 | 52 | $closeBracket = $this->tokens[ $openBracket ]['parenthesis_closer']; 53 | $firstNonEmpty = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $openBracket + 1 ), null, true ); 54 | $nextNonEmpty = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $firstNonEmpty + 1 ), null, true ); 55 | if ( $nextNonEmpty === false ) { 56 | // Parse error/live coding. Ignore. 57 | return; 58 | } 59 | 60 | if ( $this->tokens[ $firstNonEmpty ]['code'] === T_ELLIPSIS 61 | && $nextNonEmpty === $closeBracket 62 | ) { 63 | // First class callable. Ignore. 64 | return; 65 | } 66 | 67 | $variablePos = $this->isVariableAssignment( $stackPtr ); 68 | if ( $variablePos === false ) { 69 | // Not a variable assignment. 70 | return; 71 | } 72 | 73 | $variableToken = $this->tokens[ $variablePos ]; 74 | $variableName = $variableToken['content']; 75 | 76 | // Figure out the scope we need to search in. 77 | $searchEnd = $this->phpcsFile->numTokens; 78 | $functionPtr = Conditions::getLastCondition( $this->phpcsFile, $stackPtr, [ T_FUNCTION, T_CLOSURE ] ); 79 | if ( $functionPtr !== false && isset( $this->tokens[ $functionPtr ]['scope_closer'] ) ) { 80 | $searchEnd = $this->tokens[ $functionPtr ]['scope_closer']; 81 | } 82 | 83 | $nextVariableOccurrence = false; 84 | for ( $i = $closeBracket + 1; $i < $searchEnd; $i++ ) { 85 | if ( $this->tokens[ $i ]['code'] === T_VARIABLE && $this->tokens[ $i ]['content'] === $variableName ) { 86 | $nextVariableOccurrence = $i; 87 | break; 88 | } 89 | 90 | // Skip over any and all closed scopes. 91 | if ( isset( Collections::closedScopes()[ $this->tokens[ $i ]['code'] ] ) ) { 92 | if ( isset( $this->tokens[ $i ]['scope_closer'] ) ) { 93 | $i = $this->tokens[ $i ]['scope_closer']; 94 | } 95 | } 96 | } 97 | 98 | if ( $nextVariableOccurrence === false ) { 99 | return; 100 | } 101 | 102 | $rightAfterNextVariableOccurence = $this->phpcsFile->findNext( Tokens::$emptyTokens, $nextVariableOccurrence + 1, $searchEnd, true, null, true ); 103 | 104 | if ( $rightAfterNextVariableOccurence === false 105 | || $this->tokens[ $rightAfterNextVariableOccurence ]['code'] !== T_EQUAL 106 | ) { 107 | // Not a value override. 108 | return; 109 | } 110 | 111 | $valueAfterEqualSign = $this->phpcsFile->findNext( Tokens::$emptyTokens, $rightAfterNextVariableOccurence + 1, $searchEnd, true, null, true ); 112 | 113 | if ( $valueAfterEqualSign !== false && $this->tokens[ $valueAfterEqualSign ]['code'] === T_FALSE ) { 114 | $message = 'Obtained cached value in `%s` is being overridden. Disabling caching?'; 115 | $data = [ $variableName ]; 116 | $this->phpcsFile->addError( $message, $nextVariableOccurrence, 'CacheValueOverride', $data ); 117 | } 118 | } 119 | 120 | /** 121 | * Check whether the examined code is a variable assignment. 122 | * 123 | * @param int $stackPtr The position of the current token in the stack. 124 | * 125 | * @return int|false 126 | */ 127 | private function isVariableAssignment( $stackPtr ) { 128 | 129 | // Find the previous non-empty token, but allow for FQN function calls. 130 | $search = Tokens::$emptyTokens; 131 | $search[] = T_BITWISE_AND; 132 | $search[] = T_NS_SEPARATOR; 133 | $previous = $this->phpcsFile->findPrevious( $search, $stackPtr - 1, null, true ); 134 | 135 | if ( $this->tokens[ $previous ]['code'] !== T_EQUAL ) { 136 | // It's not a variable assignment. 137 | return false; 138 | } 139 | 140 | $previous = $this->phpcsFile->findPrevious( $search, $previous - 1, null, true ); 141 | 142 | if ( $this->tokens[ $previous ]['code'] !== T_VARIABLE ) { 143 | // It's not a variable assignment. 144 | return false; 145 | } 146 | 147 | return $previous; 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /WordPressVIPMinimum/Sniffs/Variables/ServerVariablesSniff.php: -------------------------------------------------------------------------------- 1 | > 25 | */ 26 | public $restrictedVariables = [ 27 | 'authVariables' => [ 28 | 'PHP_AUTH_USER' => true, 29 | 'PHP_AUTH_PW' => true, 30 | ], 31 | 'userControlledVariables' => [ 32 | 'HTTP_X_IP_TRAIL' => true, 33 | 'HTTP_X_FORWARDED_FOR' => true, 34 | 'REMOTE_ADDR' => true, 35 | ], 36 | ]; 37 | 38 | /** 39 | * Returns an array of tokens this test wants to listen for. 40 | * 41 | * @return array 42 | */ 43 | public function register() { 44 | return [ 45 | T_VARIABLE, 46 | ]; 47 | } 48 | 49 | /** 50 | * Process this test when one of its tokens is encountered 51 | * 52 | * @param int $stackPtr The position of the current token in the stack passed in $tokens. 53 | * 54 | * @return void 55 | */ 56 | public function process_token( $stackPtr ) { 57 | 58 | if ( $this->tokens[ $stackPtr ]['content'] !== '$_SERVER' 59 | && $this->tokens[ $stackPtr ]['content'] !== '$GLOBALS' 60 | ) { 61 | // Not a variable we are looking for. 62 | return; 63 | } 64 | 65 | $searchStart = $stackPtr; 66 | if ( $this->tokens[ $stackPtr ]['content'] === '$GLOBALS' ) { 67 | $globalsIndexPtr = $this->get_array_access_key( $stackPtr ); 68 | if ( $globalsIndexPtr === false ) { 69 | // Couldn't find an array index token usable for the purposes of this sniff. Bow out. 70 | return; 71 | } 72 | 73 | $globalsIndexName = TextStrings::stripQuotes( $this->tokens[ $globalsIndexPtr ]['content'] ); 74 | if ( $globalsIndexName !== '_SERVER' ) { 75 | // Not access to `$GLOBALS['_SERVER']`. 76 | return; 77 | } 78 | 79 | // Set the start point for the next array access key search to the close bracket of this array index. 80 | // No need for defensive coding as we already know there will be a valid close bracket next. 81 | $searchStart = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $globalsIndexPtr + 1 ), null, true ); 82 | } 83 | 84 | $prevNonEmpty = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $stackPtr - 1 ), null, true ); 85 | if ( $this->tokens[ $prevNonEmpty ]['code'] === T_DOUBLE_COLON ) { 86 | // Access to OO property mirroring the name of the superglobal. Not our concern. 87 | return; 88 | } 89 | 90 | $indexPtr = $this->get_array_access_key( $searchStart ); 91 | if ( $indexPtr === false ) { 92 | // Couldn't find an array index token usable for the purposes of this sniff. Bow out as undetermined. 93 | return; 94 | } 95 | 96 | $indexName = TextStrings::stripQuotes( $this->tokens[ $indexPtr ]['content'] ); 97 | 98 | if ( isset( $this->restrictedVariables['authVariables'][ $indexName ] ) ) { 99 | $message = 'Basic authentication should not be handled via PHP code.'; 100 | $this->phpcsFile->addError( $message, $stackPtr, 'BasicAuthentication' ); 101 | } elseif ( isset( $this->restrictedVariables['userControlledVariables'][ $indexName ] ) ) { 102 | $message = 'Header "%s" is user-controlled and should be properly validated before use.'; 103 | $data = [ $indexName ]; 104 | $this->phpcsFile->addError( $message, $stackPtr, 'UserControlledHeaders', $data ); 105 | } 106 | } 107 | 108 | /** 109 | * Get the array access key. 110 | * 111 | * Find the array access key and check if it is: 112 | * - comprised of a single functional token. 113 | * - that token is a T_CONSTANT_ENCAPSED_STRING. 114 | * 115 | * @param int $stackPtr The position of either a variable or the close bracket of a previous array access. 116 | * 117 | * @return int|false Stack pointer to the index token; or FALSE for 118 | * live coding, non-indexed array assignment, or non plain text array keys. 119 | */ 120 | private function get_array_access_key( $stackPtr ) { 121 | $openBracket = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true ); 122 | if ( $openBracket === false 123 | || $this->tokens[ $openBracket ]['code'] !== T_OPEN_SQUARE_BRACKET 124 | || isset( $this->tokens[ $openBracket ]['bracket_closer'] ) === false 125 | ) { 126 | // If it isn't an open bracket, this isn't array access. And without closer, it is a parse error/live coding. 127 | return false; 128 | } 129 | 130 | $closeBracket = $this->tokens[ $openBracket ]['bracket_closer']; 131 | 132 | $indexPtr = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $openBracket + 1 ), $closeBracket, true ); 133 | if ( $indexPtr === false 134 | || $this->tokens[ $indexPtr ]['code'] !== T_CONSTANT_ENCAPSED_STRING 135 | ) { 136 | // No array access (like for array assignment without key) or key is not plain text. 137 | return false; 138 | } 139 | 140 | $hasOtherTokens = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $indexPtr + 1 ), $closeBracket, true ); 141 | if ( $hasOtherTokens !== false ) { 142 | // The array index is comprised of multiple tokens. Bow out as undetermined. 143 | return false; 144 | } 145 | 146 | return $indexPtr; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /WordPressVIPMinimum/Sniffs/Security/StaticStrreplaceSniff.php: -------------------------------------------------------------------------------- 1 | Key is the function name, value irrelevant. 35 | */ 36 | protected $target_functions = [ 37 | 'str_replace' => true, 38 | ]; 39 | 40 | /** 41 | * Process the parameters of a matched function. 42 | * 43 | * @param int $stackPtr The position of the current token in the stack. 44 | * @param string $group_name The name of the group which was matched. 45 | * @param string $matched_content The token content (function name) which was matched 46 | * in lowercase. 47 | * @param array $parameters Array with information about the parameters. 48 | * 49 | * @return void 50 | */ 51 | public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) { 52 | $search_param = PassedParameters::getParameterFromStack( $parameters, 1, 'search' ); 53 | $replace_param = PassedParameters::getParameterFromStack( $parameters, 2, 'replace' ); 54 | $subject_param = PassedParameters::getParameterFromStack( $parameters, 3, 'subject' ); 55 | 56 | if ( $search_param === false || $replace_param === false || $subject_param === false ) { 57 | /* 58 | * Either an invalid function call (missing PHP required parameter); or function call 59 | * with argument unpacking; or live coding. 60 | * In all these cases, this is not the code pattern this sniff is looking for, so bow out. 61 | */ 62 | return; 63 | } 64 | 65 | foreach ( [ $search_param, $replace_param, $subject_param ] as $param ) { 66 | if ( $this->is_parameter_static_text( $param ) === false ) { 67 | // Non-static text token found. Not what we're looking for. 68 | return; 69 | } 70 | } 71 | 72 | $message = 'This code pattern is often used to run a very dangerous shell programs on your server. The code in these files needs to be reviewed, and possibly cleaned.'; 73 | $this->phpcsFile->addError( $message, $stackPtr, 'StaticStrreplace' ); 74 | } 75 | 76 | /** 77 | * Check whether the current parameter, or array item, only contains tokens which should be regarded 78 | * as a valid part of a static text string. 79 | * 80 | * @param array $param_info Array with information about a single parameter or array item. 81 | * Must be an array as returned via the PassedParameters class. 82 | * 83 | * @return bool 84 | */ 85 | private function is_parameter_static_text( $param_info ) { 86 | // List of tokens which can be skipped over without further examination. 87 | $static_tokens = [ 88 | T_CONSTANT_ENCAPSED_STRING => T_CONSTANT_ENCAPSED_STRING, 89 | T_PLUS => T_PLUS, 90 | T_STRING_CONCAT => T_STRING_CONCAT, 91 | ]; 92 | $static_tokens += Tokens::$emptyTokens; 93 | 94 | for ( $i = $param_info['start']; $i <= $param_info['end']; $i++ ) { 95 | $next_to_examine = $this->phpcsFile->findNext( $static_tokens, $i, ( $param_info['end'] + 1 ), true ); 96 | if ( $next_to_examine === false ) { 97 | // The parameter contained only tokens which could be considered static text. 98 | return true; 99 | } 100 | 101 | if ( isset( Collections::arrayOpenTokensBC()[ $this->tokens[ $next_to_examine ]['code'] ] ) ) { 102 | $arrayOpenClose = Arrays::getOpenClose( $this->phpcsFile, $next_to_examine ); 103 | if ( $arrayOpenClose === false ) { 104 | // Short list, parse error or live coding, bow out. 105 | return false; 106 | } 107 | 108 | $array_items = PassedParameters::getParameters( $this->phpcsFile, $next_to_examine ); 109 | foreach ( $array_items as $array_item ) { 110 | if ( $this->is_parameter_static_text( $array_item ) === false ) { 111 | return false; 112 | } 113 | } 114 | 115 | // The array only contained items with tokens which could be considered static text. 116 | $i = $arrayOpenClose['closer']; 117 | continue; 118 | } 119 | 120 | if ( $this->tokens[ $next_to_examine ]['code'] === T_START_HEREDOC ) { 121 | $heredoc_text = TextStrings::getCompleteTextString( $this->phpcsFile, $next_to_examine ); 122 | $stripped_text = TextStrings::stripEmbeds( $heredoc_text ); 123 | if ( $heredoc_text !== $stripped_text ) { 124 | // Heredoc with interpolated expression(s). Not a static text. 125 | return false; 126 | } 127 | } 128 | 129 | if ( ( $this->tokens[ $next_to_examine ]['code'] === T_START_HEREDOC 130 | || $this->tokens[ $next_to_examine ]['code'] === T_START_NOWDOC ) 131 | && isset( $this->tokens[ $next_to_examine ]['scope_closer'] ) 132 | ) { 133 | // No interpolation. Skip to end of a heredoc/nowdoc. 134 | $i = $this->tokens[ $next_to_examine ]['scope_closer']; 135 | continue; 136 | } 137 | 138 | // Any other token means this parameter should be regarded as non-static text. Not what we're looking for. 139 | return false; 140 | } 141 | 142 | return true; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /WordPressVIPMinimum/Sniffs/JS/HTMLExecutingFunctionsSniff.php: -------------------------------------------------------------------------------- 1 | content or target. 27 | * Value indicates whether the function's arg is the content to be inserted, or the target where the inserted 28 | * content is to be inserted before/after/replaced. For the latter, the content is in the preceding method's arg. 29 | * 30 | * @var array 31 | */ 32 | public $HTMLExecutingFunctions = [ 33 | 'after' => 'content', // jQuery. 34 | 'append' => 'content', // jQuery. 35 | 'appendTo' => 'target', // jQuery. 36 | 'before' => 'content', // jQuery. 37 | 'html' => 'content', // jQuery. 38 | 'insertAfter' => 'target', // jQuery. 39 | 'insertBefore' => 'target', // jQuery. 40 | 'prepend' => 'content', // jQuery. 41 | 'prependTo' => 'target', // jQuery. 42 | 'replaceAll' => 'target', // jQuery. 43 | 'replaceWith' => 'content', // jQuery. 44 | 'write' => 'content', 45 | 'writeln' => 'content', 46 | ]; 47 | 48 | /** 49 | * A list of tokenizers this sniff supports. 50 | * 51 | * @var string[] 52 | */ 53 | public $supportedTokenizers = [ 'JS' ]; 54 | 55 | /** 56 | * Returns an array of tokens this test wants to listen for. 57 | * 58 | * @return array 59 | */ 60 | public function register() { 61 | return [ 62 | T_STRING, 63 | ]; 64 | } 65 | 66 | /** 67 | * Processes this test, when one of its tokens is encountered. 68 | * 69 | * @param int $stackPtr The position of the current token in the stack passed in $tokens. 70 | * 71 | * @return void 72 | */ 73 | public function process_token( $stackPtr ) { 74 | 75 | if ( ! isset( $this->HTMLExecutingFunctions[ $this->tokens[ $stackPtr ]['content'] ] ) ) { 76 | // Looking for specific functions only. 77 | return; 78 | } 79 | 80 | if ( $this->HTMLExecutingFunctions[ $this->tokens[ $stackPtr ]['content'] ] === 'content' ) { 81 | $nextToken = $this->phpcsFile->findNext( Tokens::$emptyTokens, $stackPtr + 1, null, true, null, true ); 82 | 83 | if ( $this->tokens[ $nextToken ]['code'] !== T_OPEN_PARENTHESIS ) { 84 | // Not a function. 85 | return; 86 | } 87 | 88 | $parenthesis_closer = $this->tokens[ $nextToken ]['parenthesis_closer']; 89 | 90 | while ( $nextToken < $parenthesis_closer ) { 91 | $nextToken = $this->phpcsFile->findNext( Tokens::$emptyTokens, $nextToken + 1, null, true, null, true ); 92 | if ( $this->tokens[ $nextToken ]['code'] === T_STRING ) { // Contains a variable, function call or something else dynamic. 93 | $message = 'Any HTML passed to `%s` gets executed. Make sure it\'s properly escaped.'; 94 | $data = [ $this->tokens[ $stackPtr ]['content'] ]; 95 | $this->phpcsFile->addWarning( $message, $stackPtr, $this->tokens[ $stackPtr ]['content'], $data ); 96 | 97 | return; 98 | } 99 | } 100 | } elseif ( $this->HTMLExecutingFunctions[ $this->tokens[ $stackPtr ]['content'] ] === 'target' ) { 101 | $prevToken = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, $stackPtr - 1, null, true, null, true ); 102 | 103 | if ( $this->tokens[ $prevToken ]['code'] !== T_OBJECT_OPERATOR ) { 104 | return; 105 | } 106 | 107 | $prevPrevToken = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, $prevToken - 1, null, true, null, true ); 108 | 109 | if ( $this->tokens[ $prevPrevToken ]['code'] !== T_CLOSE_PARENTHESIS ) { 110 | // Not a function call, but may be a variable containing an element reference, so just 111 | // flag all remaining instances of these target HTML executing functions. 112 | $message = 'Any HTML used with `%s` gets executed. Make sure it\'s properly escaped.'; 113 | $data = [ $this->tokens[ $stackPtr ]['content'] ]; 114 | $this->phpcsFile->addWarning( $message, $stackPtr, $this->tokens[ $stackPtr ]['content'], $data ); 115 | 116 | return; 117 | } 118 | 119 | // Check if it's a function call (typically $() ) that contains a dynamic part. 120 | $parenthesis_opener = $this->tokens[ $prevPrevToken ]['parenthesis_opener']; 121 | 122 | while ( $prevPrevToken > $parenthesis_opener ) { 123 | $prevPrevToken = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, $prevPrevToken - 1, null, true, null, true ); 124 | if ( $this->tokens[ $prevPrevToken ]['code'] === T_STRING ) { // Contains a variable, function call or something else dynamic. 125 | $message = 'Any HTML used with `%s` gets executed. Make sure it\'s properly escaped.'; 126 | $data = [ $this->tokens[ $stackPtr ]['content'] ]; 127 | $this->phpcsFile->addWarning( $message, $stackPtr, $this->tokens[ $stackPtr ]['content'], $data ); 128 | 129 | return; 130 | } 131 | } 132 | } 133 | } 134 | 135 | /** 136 | * Provide the version number in which the sniff was deprecated. 137 | * 138 | * @return string 139 | */ 140 | public function getDeprecationVersion() { 141 | return 'VIP-Coding-Standard v3.1.0'; 142 | } 143 | 144 | /** 145 | * Provide the version number in which the sniff will be removed. 146 | * 147 | * @return string 148 | */ 149 | public function getRemovalVersion() { 150 | return 'VIP-Coding-Standard v4.0.0'; 151 | } 152 | 153 | /** 154 | * Provide a custom message to display with the deprecation. 155 | * 156 | * @return string 157 | */ 158 | public function getDeprecationMessage() { 159 | return 'Support for scanning JavaScript files will be removed from PHP_CodeSniffer, so this sniff is no longer viable.'; 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /WordPressVIPMinimum/ruleset-test.php: -------------------------------------------------------------------------------- 1 | [ 18 | 4 => 1, 19 | 7 => 1, 20 | 11 => 1, 21 | 16 => 1, 22 | 17 => 1, 23 | 21 => 1, 24 | 27 => 2, 25 | 35 => 1, 26 | 45 => 1, 27 | 54 => 1, 28 | 73 => 1, 29 | 88 => 1, 30 | 104 => 1, 31 | 110 => 1, 32 | 117 => 1, 33 | 118 => 1, 34 | 124 => 1, 35 | 130 => 1, 36 | 147 => 1, 37 | 150 => 1, 38 | 153 => 1, 39 | 156 => 1, 40 | 159 => 1, 41 | 162 => 1, 42 | 165 => 1, 43 | 168 => 1, 44 | 171 => 1, 45 | 174 => 1, 46 | 177 => 1, 47 | 190 => 1, 48 | 193 => 1, 49 | 204 => 1, 50 | 205 => 1, 51 | 206 => 1, 52 | 207 => 1, 53 | 208 => 1, 54 | 209 => 1, 55 | 210 => 1, 56 | 211 => 1, 57 | 212 => 1, 58 | 213 => 1, 59 | 214 => 1, 60 | 215 => 1, 61 | 216 => 1, 62 | 226 => 1, 63 | 227 => 1, 64 | 228 => 1, 65 | 229 => 1, 66 | 230 => 1, 67 | 231 => 1, 68 | 232 => 1, 69 | 233 => 1, 70 | 234 => 1, 71 | 235 => 1, 72 | 236 => 1, 73 | 237 => 1, 74 | 238 => 1, 75 | 259 => 1, 76 | 274 => 1, 77 | 285 => 1, 78 | 290 => 1, 79 | 295 => 1, 80 | 296 => 1, 81 | 299 => 1, 82 | 303 => 1, 83 | 304 => 1, 84 | 312 => 1, 85 | 313 => 1, 86 | 314 => 1, 87 | 315 => 1, 88 | 316 => 1, 89 | 317 => 1, 90 | 319 => 1, 91 | 320 => 1, 92 | 321 => 1, 93 | 322 => 1, 94 | 326 => 1, 95 | 327 => 1, 96 | 333 => 1, 97 | 334 => 1, 98 | 335 => 1, 99 | 336 => 1, 100 | 337 => 1, 101 | 338 => 1, 102 | 339 => 1, 103 | 340 => 1, 104 | 341 => 1, 105 | 342 => 1, 106 | 343 => 1, 107 | 344 => 1, 108 | 345 => 1, 109 | 346 => 1, 110 | 347 => 1, 111 | 348 => 1, 112 | 349 => 1, 113 | 350 => 1, 114 | 351 => 1, 115 | 352 => 1, 116 | 353 => 1, 117 | 354 => 1, 118 | 355 => 1, 119 | 356 => 1, 120 | 357 => 1, 121 | 358 => 1, 122 | 359 => 1, 123 | 360 => 1, 124 | 361 => 1, 125 | 362 => 1, 126 | 363 => 1, 127 | 364 => 1, 128 | 365 => 1, 129 | 366 => 1, 130 | 367 => 1, 131 | 369 => 1, 132 | 371 => 1, 133 | 372 => 1, 134 | 375 => 1, 135 | 376 => 1, 136 | 377 => 1, 137 | 378 => 1, 138 | 379 => 1, 139 | 380 => 1, 140 | 381 => 1, 141 | 382 => 1, 142 | 383 => 1, 143 | 384 => 1, 144 | 385 => 1, 145 | 386 => 1, 146 | 387 => 1, 147 | 388 => 1, 148 | 389 => 1, 149 | 390 => 1, 150 | 391 => 1, 151 | 392 => 1, 152 | 402 => 1, 153 | 415 => 1, 154 | 425 => 1, 155 | 451 => 1, 156 | 463 => 1, 157 | 465 => 1, 158 | 469 => 1, 159 | 471 => 1, 160 | 477 => 1, 161 | 483 => 1, 162 | 491 => 1, 163 | 505 => 1, 164 | 509 => 1, 165 | 510 => 1, 166 | 511 => 1, 167 | 512 => 1, 168 | 513 => 1, 169 | 514 => 1, 170 | 515 => 1, 171 | 516 => 1, 172 | 517 => 1, 173 | 518 => 1, 174 | 519 => 1, 175 | 523 => 1, 176 | 525 => 1, 177 | 550 => 1, 178 | 551 => 1, 179 | 554 => 1, 180 | 569 => 1, 181 | 570 => 1, 182 | 573 => 1, 183 | 574 => 1, 184 | 575 => 1, 185 | 578 => 1, 186 | 581 => 1, 187 | 582 => 1, 188 | 583 => 1, 189 | 588 => 1, 190 | 590 => 1, 191 | 594 => 1, 192 | 595 => 1, 193 | 596 => 1, 194 | 597 => 1, 195 | 612 => 1, 196 | 614 => 1, 197 | 621 => 1, 198 | ], 199 | 'warnings' => [ 200 | 32 => 1, 201 | 39 => 1, 202 | 41 => 1, 203 | 42 => 1, 204 | 60 => 2, 205 | 64 => 1, 206 | 67 => 1, 207 | 68 => 1, 208 | 69 => 1, 209 | 76 => 1, 210 | 80 => 1, 211 | 84 => 1, 212 | 98 => 1, 213 | 126 => 1, 214 | 138 => 1, 215 | 139 => 1, 216 | 140 => 1, 217 | 141 => 1, 218 | 142 => 1, 219 | 143 => 1, 220 | 144 => 1, 221 | 180 => 1, 222 | 181 => 1, 223 | 182 => 1, 224 | 183 => 1, 225 | 184 => 1, 226 | 185 => 1, 227 | 186 => 1, 228 | 187 => 1, 229 | 217 => 1, 230 | 239 => 1, 231 | 242 => 1, 232 | 243 => 1, 233 | 244 => 1, 234 | 245 => 1, 235 | 246 => 1, 236 | 247 => 1, 237 | 248 => 1, 238 | 249 => 1, 239 | 250 => 1, 240 | 251 => 1, 241 | 252 => 1, 242 | 253 => 1, 243 | 254 => 1, 244 | 255 => 1, 245 | 256 => 1, 246 | 264 => 2, 247 | 279 => 1, 248 | 288 => 1, 249 | 293 => 1, 250 | 294 => 1, 251 | 324 => 1, 252 | 396 => 1, 253 | 397 => 1, 254 | 398 => 1, 255 | 399 => 1, 256 | 400 => 1, 257 | 401 => 1, 258 | 403 => 1, 259 | 404 => 1, 260 | 405 => 1, 261 | 406 => 1, 262 | 407 => 1, 263 | 408 => 1, 264 | 411 => 1, 265 | 412 => 1, 266 | 432 => 1, 267 | 437 => 1, 268 | 438 => 1, 269 | 439 => 1, 270 | 440 => 1, 271 | 441 => 1, 272 | 454 => 1, 273 | 457 => 1, 274 | 458 => 1, 275 | 459 => 1, 276 | 499 => 1, 277 | 500 => 1, 278 | 504 => 1, 279 | 528 => 1, 280 | 529 => 1, 281 | 530 => 1, 282 | 531 => 1, 283 | 532 => 1, 284 | 535 => 1, 285 | 538 => 1, 286 | 545 => 1, 287 | 559 => 1, 288 | 565 => 1, 289 | 589 => 1, 290 | 618 => 1, 291 | ], 292 | 'messages' => [ 293 | 130 => [ 294 | '`eval()` is a security risk, please refrain from using it.', 295 | ], 296 | 242 => [ 297 | 'Using cURL functions is highly discouraged within VIP context. Please see: https://docs.wpvip.com/technical-references/code-quality-and-best-practices/retrieving-remote-data/.', 298 | ], 299 | 243 => [ 300 | 'Using cURL functions is highly discouraged within VIP context. Please see: https://docs.wpvip.com/technical-references/code-quality-and-best-practices/retrieving-remote-data/.', 301 | ], 302 | 244 => [ 303 | 'Using cURL functions is highly discouraged within VIP context. Please see: https://docs.wpvip.com/technical-references/code-quality-and-best-practices/retrieving-remote-data/.', 304 | ], 305 | 259 => [ 306 | '`get_children()` performs a no-LIMIT query by default, make sure to set a reasonable `posts_per_page`. `get_children()` will do a -1 query by default, a maximum of 100 should be used.', 307 | ], 308 | ], 309 | ]; 310 | 311 | require __DIR__ . '/../tests/RulesetTest.php'; 312 | 313 | // Run the tests! 314 | $test = new RulesetTest( 'WordPressVIPMinimum', $expected ); 315 | if ( $test->passes() ) { 316 | printf( 'All WordPressVIPMinimum tests passed!' . PHP_EOL ); 317 | exit( 0 ); 318 | } 319 | 320 | exit( 1 ); 321 | -------------------------------------------------------------------------------- /WordPressVIPMinimum/Sniffs/Functions/DynamicCallsSniff.php: -------------------------------------------------------------------------------- 1 | 36 | */ 37 | private $disallowed_functions = [ 38 | 'assert' => true, 39 | 'compact' => true, 40 | 'extract' => true, 41 | 'func_get_args' => true, 42 | 'func_get_arg' => true, 43 | 'func_num_args' => true, 44 | 'get_defined_vars' => true, 45 | 'mb_parse_str' => true, 46 | 'parse_str' => true, 47 | ]; 48 | 49 | /** 50 | * Potential end tokens for which the end pointer has to be set back by one. 51 | * 52 | * {@internal The PHPCS `findEndOfStatement()` method is not completely consistent 53 | * in how it returns the statement end. This is just a simple way to bypass 54 | * the inconsistency for our purposes.} 55 | * 56 | * @var array 57 | */ 58 | private $inclusiveStopPoints = [ 59 | T_COLON => true, 60 | T_COMMA => true, 61 | T_DOUBLE_ARROW => true, 62 | T_SEMICOLON => true, 63 | ]; 64 | 65 | /** 66 | * Array of variable assignments encountered, along with their values. 67 | * 68 | * Populated at run-time. 69 | * 70 | * @var array The key is the name of the variable, the value, its assigned value. 71 | */ 72 | private $variables_arr = []; 73 | 74 | /** 75 | * Returns the token types that this sniff is interested in. 76 | * 77 | * @return array 78 | */ 79 | public function register() { 80 | return [ T_VARIABLE => T_VARIABLE ]; 81 | } 82 | 83 | /** 84 | * Processes the tokens that this sniff is interested in. 85 | * 86 | * @param int $stackPtr The position in the stack where the token was found. 87 | * 88 | * @return void 89 | */ 90 | public function process_token( $stackPtr ) { 91 | // First collect all variables encountered and their values. 92 | $this->collect_variables( $stackPtr ); 93 | 94 | // Then find all dynamic calls, and report them. 95 | $this->find_dynamic_calls( $stackPtr ); 96 | } 97 | 98 | /** 99 | * Finds any variable-definitions in the file being processed and stores them 100 | * internally in a private array. 101 | * 102 | * @param int $stackPtr The position in the stack where the token was found. 103 | * 104 | * @return void 105 | */ 106 | private function collect_variables( $stackPtr ) { 107 | 108 | $current_var_name = $this->tokens[ $stackPtr ]['content']; 109 | 110 | /* 111 | * Find assignments ( $foo = "bar"; ) by finding all non-whitespaces, 112 | * and checking if the first one is T_EQUAL. 113 | */ 114 | $t_item_key = $this->phpcsFile->findNext( Tokens::$emptyTokens, $stackPtr + 1, null, true ); 115 | if ( $t_item_key === false || $this->tokens[ $t_item_key ]['code'] !== T_EQUAL ) { 116 | return; 117 | } 118 | 119 | /* 120 | * Find assignments which only assign a plain text string. 121 | */ 122 | $end_of_statement = $this->phpcsFile->findEndOfStatement( ( $t_item_key + 1 ) ); 123 | if ( isset( $this->inclusiveStopPoints[ $this->tokens[ $end_of_statement ]['code'] ] ) === true ) { 124 | --$end_of_statement; 125 | } 126 | 127 | $value_ptr = null; 128 | for ( $i = $t_item_key + 1; $i <= $end_of_statement; $i++ ) { 129 | if ( isset( Tokens::$emptyTokens[ $this->tokens[ $i ]['code'] ] ) === true ) { 130 | continue; 131 | } 132 | 133 | if ( $this->tokens[ $i ]['code'] !== T_CONSTANT_ENCAPSED_STRING ) { 134 | // Not a plain text string value. Value cannot be determined reliably. 135 | return; 136 | } 137 | 138 | $value_ptr = $i; 139 | } 140 | 141 | if ( isset( $value_ptr ) === false ) { 142 | // Parse error. Bow out. 143 | return; 144 | } 145 | 146 | /* 147 | * If we reached the end of the loop and the $value_ptr was set, we know for sure 148 | * this was a plain text string variable assignment. 149 | */ 150 | $current_var_value = TextStrings::stripQuotes( $this->tokens[ $value_ptr ]['content'] ); 151 | 152 | if ( isset( $this->disallowed_functions[ $current_var_value ] ) === false ) { 153 | // Text string is not one of the ones we're looking for. 154 | return; 155 | } 156 | 157 | /* 158 | * Register the variable name and value in the internal array for later usage. 159 | */ 160 | $this->variables_arr[ $current_var_name ] = $current_var_value; 161 | } 162 | 163 | /** 164 | * Find any dynamic calls being made using variables. 165 | * 166 | * Report on this when found, using the name of the function in the message. 167 | * 168 | * @param int $stackPtr The position in the stack where the token was found. 169 | * 170 | * @return void 171 | */ 172 | private function find_dynamic_calls( $stackPtr ) { 173 | // No variables detected; no basis for doing anything. 174 | if ( empty( $this->variables_arr ) ) { 175 | return; 176 | } 177 | 178 | /* 179 | * If variable is not found in our registry of variables, do nothing, as we cannot be 180 | * sure that the function being called is one of the disallowed ones. 181 | */ 182 | if ( ! isset( $this->variables_arr[ $this->tokens[ $stackPtr ]['content'] ] ) ) { 183 | return; 184 | } 185 | 186 | /* 187 | * Check if we have an '(' next. 188 | */ 189 | $next = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true ); 190 | if ( $next === false || $this->tokens[ $next ]['code'] !== T_OPEN_PARENTHESIS ) { 191 | return; 192 | } 193 | 194 | $message = 'Dynamic calling is not recommended in the case of %s().'; 195 | $data = [ $this->variables_arr[ $this->tokens[ $stackPtr ]['content'] ] ]; 196 | $this->phpcsFile->addError( $message, $stackPtr, 'DynamicCalls', $data ); 197 | } 198 | 199 | /** 200 | * Provide the version number in which the sniff was deprecated. 201 | * 202 | * @return string 203 | */ 204 | public function getDeprecationVersion() { 205 | return 'VIP-Coding-Standard v3.1.0'; 206 | } 207 | 208 | /** 209 | * Provide the version number in which the sniff will be removed. 210 | * 211 | * @return string 212 | */ 213 | public function getRemovalVersion() { 214 | return 'VIP-Coding-Standard v4.0.0'; 215 | } 216 | 217 | /** 218 | * Provide a custom message to display with the deprecation. 219 | * 220 | * @return string 221 | */ 222 | public function getDeprecationMessage() { 223 | return ''; 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /WordPressVIPMinimum/Sniffs/Performance/LowExpiryCacheTimeSniff.php: -------------------------------------------------------------------------------- 1 | Key is the function name, value irrelevant. 38 | */ 39 | protected $target_functions = [ 40 | 'wp_cache_set' => true, 41 | 'wp_cache_add' => true, 42 | 'wp_cache_replace' => true, 43 | ]; 44 | 45 | /** 46 | * List of WP time constants, see https://codex.wordpress.org/Easier_Expression_of_Time_Constants. 47 | * 48 | * @var array 49 | */ 50 | protected $wp_time_constants = [ 51 | 'MINUTE_IN_SECONDS' => 60, 52 | 'HOUR_IN_SECONDS' => 3600, 53 | 'DAY_IN_SECONDS' => 86400, 54 | 'WEEK_IN_SECONDS' => 604800, 55 | 'MONTH_IN_SECONDS' => 2592000, 56 | 'YEAR_IN_SECONDS' => 31536000, 57 | ]; 58 | 59 | /** 60 | * Process the parameters of a matched function. 61 | * 62 | * @param int $stackPtr The position of the current token in the stack. 63 | * @param string $group_name The name of the group which was matched. 64 | * @param string $matched_content The token content (function name) which was matched 65 | * in lowercase. 66 | * @param array $parameters Array with information about the parameters. 67 | * 68 | * @return int|void Integer stack pointer to skip forward or void to continue 69 | * normal file processing. 70 | */ 71 | public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) { 72 | $expire_param = PassedParameters::getParameterFromStack( $parameters, 4, 'expire' ); 73 | if ( $expire_param === false ) { 74 | // If no cache expiry time, bail (i.e. we don't want to flag for something like feeds where it is cached indefinitely until a hook runs). 75 | return; 76 | } 77 | 78 | $tokensAsString = ''; 79 | $reportPtr = null; 80 | $openParens = 0; 81 | 82 | $message = 'Cache expiry time could not be determined. Please inspect that the fourth parameter passed to %s() evaluates to 300 seconds or more. Found: "%s"'; 83 | $error_code = 'CacheTimeUndetermined'; 84 | $data = [ $matched_content, $expire_param['clean'] ]; 85 | 86 | for ( $i = $expire_param['start']; $i <= $expire_param['end']; $i++ ) { 87 | if ( isset( Tokens::$emptyTokens[ $this->tokens[ $i ]['code'] ] ) === true ) { 88 | $tokensAsString .= ' '; 89 | continue; 90 | } 91 | 92 | if ( $this->tokens[ $i ]['code'] === T_NS_SEPARATOR ) { 93 | /* 94 | * Ignore namespace separators. If it's part of a global WP time constant, it will be 95 | * handled correctly. If it's used in any other context, another token *will* trigger the 96 | * "undetermined" warning anyway. 97 | */ 98 | continue; 99 | } 100 | 101 | if ( isset( $reportPtr ) === false ) { 102 | // Set the report pointer to the first non-empty token we encounter. 103 | $reportPtr = $i; 104 | } 105 | 106 | if ( $this->tokens[ $i ]['code'] === T_LNUMBER 107 | || $this->tokens[ $i ]['code'] === T_DNUMBER 108 | ) { 109 | // Make sure that PHP 7.4 numeric literals and PHP 8.1 explicit octals don't cause problems. 110 | $number_info = Numbers::getCompleteNumber( $this->phpcsFile, $i ); 111 | $tokensAsString .= $number_info['decimal']; 112 | $i = $number_info['last_token']; 113 | continue; 114 | } 115 | 116 | if ( $this->tokens[ $i ]['code'] === T_FALSE 117 | || $this->tokens[ $i ]['code'] === T_NULL 118 | ) { 119 | $tokensAsString .= 0; 120 | continue; 121 | } 122 | 123 | if ( $this->tokens[ $i ]['code'] === T_TRUE ) { 124 | $tokensAsString .= 1; 125 | continue; 126 | } 127 | 128 | if ( isset( Tokens::$arithmeticTokens[ $this->tokens[ $i ]['code'] ] ) === true ) { 129 | $tokensAsString .= $this->tokens[ $i ]['content']; 130 | continue; 131 | } 132 | 133 | // If using time constants, we need to convert to a number. 134 | if ( $this->tokens[ $i ]['code'] === T_STRING 135 | && isset( $this->wp_time_constants[ $this->tokens[ $i ]['content'] ] ) === true 136 | ) { 137 | $tokensAsString .= $this->wp_time_constants[ $this->tokens[ $i ]['content'] ]; 138 | continue; 139 | } 140 | 141 | if ( $this->tokens[ $i ]['code'] === T_OPEN_PARENTHESIS ) { 142 | $tokensAsString .= $this->tokens[ $i ]['content']; 143 | ++$openParens; 144 | continue; 145 | } 146 | 147 | if ( $this->tokens[ $i ]['code'] === T_CLOSE_PARENTHESIS ) { 148 | $tokensAsString .= $this->tokens[ $i ]['content']; 149 | --$openParens; 150 | continue; 151 | } 152 | 153 | if ( $this->tokens[ $i ]['code'] === T_CONSTANT_ENCAPSED_STRING ) { 154 | $content = TextStrings::stripQuotes( $this->tokens[ $i ]['content'] ); 155 | if ( is_numeric( $content ) === true ) { 156 | $tokensAsString .= $content; 157 | continue; 158 | } 159 | } 160 | 161 | // Encountered an unexpected token. Manual inspection needed. 162 | $this->phpcsFile->addWarning( $message, $reportPtr, $error_code, $data ); 163 | 164 | return; 165 | } 166 | 167 | if ( $tokensAsString === '' ) { 168 | // Nothing found to evaluate. 169 | return; 170 | } 171 | 172 | $tokensAsString = trim( $tokensAsString ); 173 | 174 | if ( $openParens !== 0 ) { 175 | /* 176 | * Shouldn't be possible as that would indicate a parse error in the original code, 177 | * but let's prevent getting parse errors in the `eval`-ed code. 178 | */ 179 | if ( $openParens > 0 ) { 180 | $tokensAsString .= str_repeat( ')', $openParens ); 181 | } else { 182 | $tokensAsString = str_repeat( '(', abs( $openParens ) ) . $tokensAsString; 183 | } 184 | } 185 | 186 | $time = @eval( "return $tokensAsString;" ); // phpcs:ignore Squiz.PHP.Eval,WordPress.PHP.NoSilencedErrors -- No harm here. 187 | 188 | if ( $time === false ) { 189 | /* 190 | * The eval resulted in a parse error. This will only happen for backfilled 191 | * arithmetic operator tokens, like T_POW, on PHP versions in which the token 192 | * did not exist. In that case, flag for manual inspection. 193 | */ 194 | $this->phpcsFile->addWarning( $message, $reportPtr, $error_code, $data ); 195 | return; 196 | } 197 | 198 | if ( $time < 300 && (int) $time !== 0 ) { 199 | $message = 'Low cache expiry time of %s seconds detected. It is recommended to have 300 seconds or more.'; 200 | $data = [ $time ]; 201 | 202 | if ( (string) $time !== $tokensAsString ) { 203 | $message .= ' Found: "%s"'; 204 | $data[] = $tokensAsString; 205 | } 206 | 207 | $this->phpcsFile->addWarning( $message, $reportPtr, 'LowCacheTime', $data ); 208 | } 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /WordPressVIPMinimum/Sniffs/AbstractVariableRestrictionsSniff.php: -------------------------------------------------------------------------------- 1 | 63 | */ 64 | public function register() { 65 | // Retrieve the groups only once and don't set up a listener if there are no groups. 66 | if ( $this->setup_groups() === false ) { 67 | return []; 68 | } 69 | 70 | return [ 71 | \T_VARIABLE, 72 | \T_OBJECT_OPERATOR, 73 | \T_DOUBLE_COLON, 74 | \T_OPEN_SQUARE_BRACKET, 75 | \T_DOUBLE_QUOTED_STRING, 76 | \T_HEREDOC, 77 | ]; 78 | } 79 | 80 | /** 81 | * Groups of variables to restrict. 82 | * 83 | * This method should be overridden in extending classes. 84 | * 85 | * Example: groups => array( 86 | * 'wpdb' => array( 87 | * 'type' => 'error' | 'warning', 88 | * 'message' => 'Dont use this one please!', 89 | * 'variables' => array( '$val', '$var' ), 90 | * 'object_vars' => array( '$foo->bar', .. ), 91 | * 'array_members' => array( '$foo['bar']', .. ), 92 | * ) 93 | * ) 94 | * 95 | * @return array>> 96 | */ 97 | abstract public function getGroups(); 98 | 99 | /** 100 | * Cache the groups. 101 | * 102 | * @return bool True if the groups were setup. False if not. 103 | */ 104 | protected function setup_groups() { 105 | $this->groups_cache = $this->getGroups(); 106 | 107 | if ( empty( $this->groups_cache ) && empty( self::$groups ) ) { 108 | return false; 109 | } 110 | 111 | // Allow for adding extra unit tests. 112 | if ( ! empty( self::$groups ) ) { 113 | $this->groups_cache = array_merge( $this->groups_cache, self::$groups ); 114 | } 115 | 116 | return true; 117 | } 118 | 119 | /** 120 | * Processes this test, when one of its tokens is encountered. 121 | * 122 | * @param int $stackPtr The position of the current token in the stack. 123 | * @return int|void Integer stack pointer to skip forward or void to continue 124 | * normal file processing. 125 | */ 126 | public function process_token( $stackPtr ) { 127 | 128 | $token = $this->tokens[ $stackPtr ]; 129 | 130 | $this->excluded_groups = RulesetPropertyHelper::merge_custom_array( $this->exclude ); 131 | if ( array_diff_key( $this->groups_cache, $this->excluded_groups ) === [] ) { 132 | // All groups have been excluded. 133 | // Don't remove the listener as the exclude property can be changed inline. 134 | return; 135 | } 136 | 137 | // Check if it is a function not a variable. 138 | if ( \in_array( $token['code'], [ \T_OBJECT_OPERATOR, \T_DOUBLE_COLON ], true ) ) { // This only works for object vars and array members. 139 | $method = $this->phpcsFile->findNext( \T_WHITESPACE, $stackPtr + 1, null, true ); 140 | $possible_parenthesis = $this->phpcsFile->findNext( \T_WHITESPACE, $method + 1, null, true ); 141 | if ( $this->tokens[ $possible_parenthesis ]['code'] === \T_OPEN_PARENTHESIS ) { 142 | return; // So .. it is a function after all ! 143 | } 144 | } 145 | 146 | if ( ContextHelper::is_in_isset_or_empty( $this->phpcsFile, $stackPtr ) === true ) { 147 | // Checking whether a variable exists is not the same as using it. 148 | return; 149 | } 150 | 151 | foreach ( $this->groups_cache as $groupName => $group ) { 152 | 153 | if ( isset( $this->excluded_groups[ $groupName ] ) ) { 154 | continue; 155 | } 156 | 157 | $patterns = []; 158 | 159 | // Simple variable. 160 | if ( \in_array( $token['code'], [ \T_VARIABLE, \T_DOUBLE_QUOTED_STRING, \T_HEREDOC ], true ) && ! empty( $group['variables'] ) ) { 161 | $patterns = array_merge( $patterns, $group['variables'] ); 162 | $var = $token['content']; 163 | 164 | } 165 | 166 | if ( \in_array( $token['code'], [ \T_OBJECT_OPERATOR, \T_DOUBLE_COLON, \T_DOUBLE_QUOTED_STRING, \T_HEREDOC ], true ) && ! empty( $group['object_vars'] ) ) { 167 | // Object var, ex: $foo->bar / $foo::bar / Foo::bar / Foo::$bar . 168 | $patterns = array_merge( $patterns, $group['object_vars'] ); 169 | 170 | $owner = $this->phpcsFile->findPrevious( [ \T_VARIABLE, \T_STRING ], $stackPtr ); 171 | $child = $this->phpcsFile->findNext( [ \T_STRING, \T_VARIABLE ], $stackPtr ); 172 | $var = implode( '', [ $this->tokens[ $owner ]['content'], $token['content'], $this->tokens[ $child ]['content'] ] ); 173 | 174 | } 175 | 176 | if ( \in_array( $token['code'], [ \T_OPEN_SQUARE_BRACKET, \T_DOUBLE_QUOTED_STRING, \T_HEREDOC ], true ) && ! empty( $group['array_members'] ) ) { 177 | // Array members. 178 | $patterns = array_merge( $patterns, $group['array_members'] ); 179 | 180 | if ( isset( $token['bracket_closer'] ) ) { 181 | $owner = $this->phpcsFile->findPrevious( \T_VARIABLE, $stackPtr ); 182 | $inside = GetTokensAsString::normal( $this->phpcsFile, $stackPtr, $token['bracket_closer'] ); 183 | $var = implode( '', [ $this->tokens[ $owner ]['content'], $inside ] ); 184 | } 185 | } 186 | 187 | if ( empty( $patterns ) ) { 188 | continue; 189 | } 190 | 191 | $patterns = array_map( [ $this, 'test_patterns' ], $patterns ); 192 | $pattern = implode( '|', $patterns ); 193 | $delim = ( $token['code'] !== \T_OPEN_SQUARE_BRACKET && $token['code'] !== \T_HEREDOC ) ? '\b' : ''; 194 | 195 | if ( $token['code'] === \T_DOUBLE_QUOTED_STRING || $token['code'] === \T_HEREDOC ) { 196 | $var = $token['content']; 197 | } 198 | 199 | if ( empty( $var ) || preg_match( '#(' . $pattern . ')' . $delim . '#', $var, $match ) !== 1 ) { 200 | continue; 201 | } 202 | 203 | $code = MessageHelper::stringToErrorcode( $groupName . '_' . $match[1] ); 204 | MessageHelper::addMessage( 205 | $this->phpcsFile, 206 | $group['message'], 207 | $stackPtr, 208 | $group['type'] === 'error', 209 | $code, 210 | [ $var ] 211 | ); 212 | 213 | return; // Show one error only. 214 | } 215 | } 216 | 217 | /** 218 | * Transform a wildcard pattern to a usable regex pattern. 219 | * 220 | * @param string $pattern Pattern. 221 | * @return string 222 | */ 223 | private function test_patterns( $pattern ) { 224 | $pattern = preg_quote( $pattern, '#' ); 225 | $pattern = preg_replace( 226 | [ '#\\\\\*#', '[\'"]' ], 227 | [ '.*', '\'' ], 228 | $pattern 229 | ); 230 | return $pattern; 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /WordPressVIPMinimum/Sniffs/Files/IncludingFileSniff.php: -------------------------------------------------------------------------------- 1 | 26 | */ 27 | public $getPathFunctions = [ 28 | 'dirname', 29 | 'get_404_template', 30 | 'get_archive_template', 31 | 'get_attachment_template', 32 | 'get_author_template', 33 | 'get_category_template', 34 | 'get_date_template', 35 | 'get_embed_template', 36 | 'get_front_page_template', 37 | 'get_page_template', 38 | 'get_paged_template', // Deprecated, but should still be accepted for the purpose of this sniff. 39 | 'get_home_template', 40 | 'get_index_template', 41 | 'get_parent_theme_file_path', 42 | 'get_privacy_policy_template', 43 | 'get_query_template', 44 | 'get_search_template', 45 | 'get_single_template', 46 | 'get_singular_template', 47 | 'get_stylesheet_directory', 48 | 'get_tag_template', 49 | 'get_taxonomy_template', 50 | 'get_template_directory', 51 | 'get_theme_file_path', 52 | 'locate_block_template', 53 | 'locate_template', 54 | 'plugin_dir_path', 55 | ]; 56 | 57 | /** 58 | * List of restricted constants. 59 | * 60 | * @var array 61 | */ 62 | public $restrictedConstants = [ 63 | 'TEMPLATEPATH' => 'get_template_directory', 64 | 'STYLESHEETPATH' => 'get_stylesheet_directory', 65 | ]; 66 | 67 | /** 68 | * List of allowed constants. 69 | * 70 | * @var array 71 | */ 72 | public $allowedConstants = [ 73 | 'ABSPATH', 74 | 'WP_CONTENT_DIR', 75 | 'WP_PLUGIN_DIR', 76 | ]; 77 | 78 | /** 79 | * List of keywords allowed for use in custom constants. 80 | * Note: Customizing this property will overwrite current default values. 81 | * 82 | * @var array 83 | */ 84 | public $allowedKeywords = [ 85 | 'PATH', 86 | 'DIR', 87 | ]; 88 | 89 | /** 90 | * Functions used for modify slashes. 91 | * 92 | * @var array 93 | */ 94 | public $slashingFunctions = [ 95 | 'trailingslashit', 96 | 'user_trailingslashit', 97 | 'untrailingslashit', 98 | ]; 99 | 100 | /** 101 | * Groups of functions to restrict. 102 | * 103 | * @return array 104 | */ 105 | public function getGroups() { 106 | return []; 107 | } 108 | 109 | /** 110 | * Returns an array of tokens this test wants to listen for. 111 | * 112 | * @return array 113 | */ 114 | public function register() { 115 | return Tokens::$includeTokens; 116 | } 117 | 118 | /** 119 | * Processes this test, when one of its tokens is encountered. 120 | * 121 | * @param int $stackPtr The position of the current token in the stack. 122 | * 123 | * @return void 124 | */ 125 | public function process_token( $stackPtr ) { 126 | $nextToken = $this->phpcsFile->findNext( Tokens::$emptyTokens, $stackPtr + 1, null, true, null, true ); 127 | 128 | if ( $this->tokens[ $nextToken ]['code'] === T_OPEN_PARENTHESIS ) { 129 | // The construct is using parenthesis, grab the next non empty token. 130 | $nextToken = $this->phpcsFile->findNext( Tokens::$emptyTokens, $nextToken + 1, null, true, null, true ); 131 | } 132 | 133 | if ( $this->tokens[ $nextToken ]['code'] === T_DIR || $this->tokens[ $nextToken ]['content'] === '__DIR__' ) { 134 | // The construct is using __DIR__ which is fine. 135 | return; 136 | } 137 | 138 | if ( $this->tokens[ $nextToken ]['code'] === T_VARIABLE ) { 139 | $message = 'File inclusion using variable (`%s`). Probably needs manual inspection.'; 140 | $data = [ $this->tokens[ $nextToken ]['content'] ]; 141 | $this->phpcsFile->addWarning( $message, $nextToken, 'UsingVariable', $data ); 142 | return; 143 | } 144 | 145 | if ( $this->tokens[ $nextToken ]['code'] === T_STRING ) { 146 | if ( in_array( $this->tokens[ $nextToken ]['content'], $this->getPathFunctions, true ) === true ) { 147 | // The construct is using one of the functions for getting correct path which is fine. 148 | return; 149 | } 150 | 151 | if ( in_array( $this->tokens[ $nextToken ]['content'], $this->allowedConstants, true ) === true ) { 152 | // The construct is using one of the allowed constants which is fine. 153 | return; 154 | } 155 | 156 | if ( $this->has_custom_path( $this->tokens[ $nextToken ]['content'] ) === true ) { 157 | // The construct is using a constant with an allowed keyword. 158 | return; 159 | } 160 | 161 | if ( array_key_exists( $this->tokens[ $nextToken ]['content'], $this->restrictedConstants ) === true ) { 162 | // The construct is using one of the restricted constants. 163 | $message = '`%s` constant might not be defined or available. Use `%s()` instead.'; 164 | $data = [ $this->tokens[ $nextToken ]['content'], $this->restrictedConstants[ $this->tokens[ $nextToken ]['content'] ] ]; 165 | $this->phpcsFile->addError( $message, $nextToken, 'RestrictedConstant', $data ); 166 | return; 167 | } 168 | 169 | $nextNextToken = $this->phpcsFile->findNext( array_merge( Tokens::$emptyTokens, [ T_COMMENT ] ), $nextToken + 1, null, true, null, true ); 170 | if ( preg_match( '/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $this->tokens[ $nextToken ]['content'] ) === 1 && $this->tokens[ $nextNextToken ]['code'] !== T_OPEN_PARENTHESIS ) { 171 | // The construct is using custom constant, which needs manual inspection. 172 | $message = 'File inclusion using custom constant (`%s`). Probably needs manual inspection.'; 173 | $data = [ $this->tokens[ $nextToken ]['content'] ]; 174 | $this->phpcsFile->addWarning( $message, $nextToken, 'UsingCustomConstant', $data ); 175 | return; 176 | } 177 | 178 | if ( strpos( $this->tokens[ $nextToken ]['content'], '$' ) === 0 ) { 179 | $message = 'File inclusion using variable (`%s`). Probably needs manual inspection.'; 180 | $data = [ $this->tokens[ $nextToken ]['content'] ]; 181 | $this->phpcsFile->addWarning( $message, $nextToken, 'UsingVariable', $data ); 182 | return; 183 | } 184 | 185 | if ( in_array( $this->tokens[ $nextToken ]['content'], $this->slashingFunctions, true ) === true ) { 186 | // The construct is using one of the slashing functions, it's probably correct. 187 | return; 188 | } 189 | 190 | if ( $this->is_targetted_token( $nextToken ) ) { 191 | $message = 'File inclusion using custom function ( `%s()` ). Must return local file source, as external URLs are prohibited on WordPress VIP. Probably needs manual inspection.'; 192 | $data = [ $this->tokens[ $nextToken ]['content'] ]; 193 | $this->phpcsFile->addWarning( $message, $nextToken, 'UsingCustomFunction', $data ); 194 | return; 195 | } 196 | 197 | $message = 'Absolute include path must be used. Use `get_template_directory()`, `get_stylesheet_directory()` or `plugin_dir_path()`.'; 198 | $this->phpcsFile->addError( $message, $nextToken, 'NotAbsolutePath' ); 199 | return; 200 | } 201 | 202 | if ( $this->tokens[ $nextToken ]['code'] === T_CONSTANT_ENCAPSED_STRING && filter_var( str_replace( [ '"', "'" ], '', $this->tokens[ $nextToken ]['content'] ), FILTER_VALIDATE_URL ) ) { 203 | $message = 'Include path must be local file source, external URLs are prohibited on WordPress VIP.'; 204 | $this->phpcsFile->addError( $message, $nextToken, 'ExternalURL' ); 205 | return; 206 | } 207 | 208 | $message = 'Absolute include path must be used. Use `get_template_directory()`, `get_stylesheet_directory()` or `plugin_dir_path()`.'; 209 | $this->phpcsFile->addError( $message, $nextToken, 'NotAbsolutePath' ); 210 | } 211 | 212 | /** 213 | * Check if a content string contains a keyword in custom paths. 214 | * 215 | * @param string $content Content string. 216 | * 217 | * @return bool True if the string partially matches a keyword in $allowedCustomKeywords, false otherwise. 218 | */ 219 | private function has_custom_path( $content ) { 220 | foreach ( $this->allowedKeywords as $keyword ) { 221 | if ( strpos( $content, $keyword ) !== false ) { 222 | return true; 223 | } 224 | } 225 | 226 | return false; 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /WordPressVIPMinimum/Sniffs/Hooks/AlwaysReturnInFilterSniff.php: -------------------------------------------------------------------------------- 1 | 33 | */ 34 | public function register() { 35 | return [ T_STRING ]; 36 | } 37 | 38 | /** 39 | * Processes the tokens that this sniff is interested in. 40 | * 41 | * @param int $stackPtr The position in the stack where the token was found. 42 | * 43 | * @return void 44 | */ 45 | public function process_token( $stackPtr ) { 46 | 47 | $functionName = $this->tokens[ $stackPtr ]['content']; 48 | 49 | if ( $functionName !== 'add_filter' ) { 50 | return; 51 | } 52 | 53 | $this->filterNamePtr = $this->phpcsFile->findNext( 54 | array_merge( Tokens::$emptyTokens, [ T_OPEN_PARENTHESIS ] ), 55 | $stackPtr + 1, 56 | null, 57 | true, 58 | null, 59 | true 60 | ); 61 | 62 | if ( ! $this->filterNamePtr ) { 63 | // Something is wrong. 64 | return; 65 | } 66 | 67 | $callbackPtr = $this->phpcsFile->findNext( 68 | array_merge( Tokens::$emptyTokens, [ T_COMMA ] ), 69 | $this->filterNamePtr + 1, 70 | null, 71 | true, 72 | null, 73 | true 74 | ); 75 | 76 | if ( ! $callbackPtr ) { 77 | // Something is wrong. 78 | return; 79 | } 80 | 81 | if ( $this->tokens[ $callbackPtr ]['code'] === T_CLOSURE ) { 82 | $this->processFunctionBody( $callbackPtr ); 83 | } elseif ( $this->tokens[ $callbackPtr ]['code'] === T_ARRAY 84 | || $this->tokens[ $callbackPtr ]['code'] === T_OPEN_SHORT_ARRAY 85 | ) { 86 | $this->processArray( $callbackPtr ); 87 | } elseif ( in_array( $this->tokens[ $callbackPtr ]['code'], Tokens::$stringTokens, true ) === true ) { 88 | $this->processString( $callbackPtr ); 89 | } 90 | } 91 | 92 | /** 93 | * Process array. 94 | * 95 | * @param int $stackPtr The position in the stack where the token was found. 96 | * 97 | * @return void 98 | */ 99 | private function processArray( $stackPtr ) { 100 | 101 | $open_close = Arrays::getOpenClose( $this->phpcsFile, $stackPtr ); 102 | if ( $open_close === false ) { 103 | return; 104 | } 105 | 106 | $previous = $this->phpcsFile->findPrevious( 107 | Tokens::$emptyTokens, 108 | $open_close['closer'] - 1, 109 | null, 110 | true 111 | ); 112 | 113 | if ( in_array( T_CLASS, $this->tokens[ $stackPtr ]['conditions'], true ) === true ) { 114 | $classPtr = array_search( T_CLASS, $this->tokens[ $stackPtr ]['conditions'], true ); 115 | if ( $classPtr ) { 116 | $classToken = $this->tokens[ $classPtr ]; 117 | $this->processString( $previous, $classToken['scope_opener'], $classToken['scope_closer'] ); 118 | return; 119 | } 120 | } 121 | 122 | $this->processString( $previous ); 123 | } 124 | 125 | /** 126 | * Process string. 127 | * 128 | * @param int $stackPtr The position in the stack where the token was found. 129 | * @param int $start The start of the token. 130 | * @param int $end The end of the token. 131 | * 132 | * @return void 133 | */ 134 | private function processString( $stackPtr, $start = 0, $end = null ) { 135 | 136 | $callbackFunctionName = substr( $this->tokens[ $stackPtr ]['content'], 1, -1 ); 137 | 138 | $callbackFunctionPtr = $this->phpcsFile->findNext( 139 | T_STRING, 140 | $start, 141 | $end, 142 | false, 143 | $callbackFunctionName 144 | ); 145 | 146 | if ( ! $callbackFunctionPtr ) { 147 | // We were not able to find the function callback in the file. 148 | return; 149 | } 150 | 151 | $this->processFunction( $callbackFunctionPtr, $start, $end ); 152 | } 153 | 154 | /** 155 | * Process function. 156 | * 157 | * @param int $stackPtr The position in the stack where the token was found. 158 | * @param int $start The start of the token. 159 | * @param int $end The end of the token. 160 | * 161 | * @return void 162 | */ 163 | private function processFunction( $stackPtr, $start = 0, $end = null ) { 164 | 165 | $functionName = $this->tokens[ $stackPtr ]['content']; 166 | 167 | $offset = $start; 168 | while ( $this->phpcsFile->findNext( [ T_FUNCTION ], $offset, $end ) !== false ) { 169 | $functionStackPtr = $this->phpcsFile->findNext( [ T_FUNCTION ], $offset, $end ); 170 | $functionNamePtr = $this->phpcsFile->findNext( Tokens::$emptyTokens, $functionStackPtr + 1, null, true, null, true ); 171 | if ( $this->tokens[ $functionNamePtr ]['code'] === T_STRING && $this->tokens[ $functionNamePtr ]['content'] === $functionName ) { 172 | $this->processFunctionBody( $functionStackPtr ); 173 | return; 174 | } 175 | $offset = $functionStackPtr + 1; 176 | } 177 | } 178 | 179 | /** 180 | * Process function's body 181 | * 182 | * @param int $stackPtr The position in the stack where the token was found. 183 | * 184 | * @return void 185 | */ 186 | private function processFunctionBody( $stackPtr ) { 187 | 188 | $filterName = $this->tokens[ $this->filterNamePtr ]['content']; 189 | 190 | $methodProps = FunctionDeclarations::getProperties( $this->phpcsFile, $stackPtr ); 191 | if ( $methodProps['is_abstract'] === true ) { 192 | $message = 'The callback for the `%s` filter hook-in points to an abstract method. Please ensure that child class implementations of this method always return a value.'; 193 | $data = [ $filterName ]; 194 | $this->phpcsFile->addWarning( $message, $stackPtr, 'AbstractMethod', $data ); 195 | return; 196 | } 197 | 198 | if ( isset( $this->tokens[ $stackPtr ]['scope_opener'], $this->tokens[ $stackPtr ]['scope_closer'] ) === false ) { 199 | // Live coding, parse or tokenizer error. 200 | return; 201 | } 202 | 203 | $argPtr = $this->phpcsFile->findNext( 204 | array_merge( Tokens::$emptyTokens, [ T_STRING, T_OPEN_PARENTHESIS ] ), 205 | $stackPtr + 1, 206 | null, 207 | true, 208 | null, 209 | true 210 | ); 211 | 212 | // If arg is being passed by reference, we can skip. 213 | if ( $this->tokens[ $argPtr ]['code'] === T_BITWISE_AND ) { 214 | return; 215 | } 216 | 217 | $functionBodyScopeStart = $this->tokens[ $stackPtr ]['scope_opener']; 218 | $functionBodyScopeEnd = $this->tokens[ $stackPtr ]['scope_closer']; 219 | 220 | $returnTokenPtr = $this->phpcsFile->findNext( 221 | [ T_RETURN ], 222 | $functionBodyScopeStart + 1, 223 | $functionBodyScopeEnd 224 | ); 225 | 226 | $outsideConditionalReturn = 0; 227 | 228 | while ( $returnTokenPtr ) { 229 | if ( $this->isInsideIfConditonal( $returnTokenPtr ) === false ) { 230 | ++$outsideConditionalReturn; 231 | } 232 | if ( $this->isReturningVoid( $returnTokenPtr ) ) { 233 | $message = 'Please, make sure that a callback to `%s` filter is returning void intentionally.'; 234 | $data = [ $filterName ]; 235 | $this->phpcsFile->addError( $message, $functionBodyScopeStart, 'VoidReturn', $data ); 236 | } 237 | $returnTokenPtr = $this->phpcsFile->findNext( 238 | [ T_RETURN ], 239 | $returnTokenPtr + 1, 240 | $functionBodyScopeEnd 241 | ); 242 | } 243 | 244 | if ( $outsideConditionalReturn === 0 ) { 245 | $message = 'Please, make sure that a callback to `%s` filter is always returning some value.'; 246 | $data = [ $filterName ]; 247 | $this->phpcsFile->addError( $message, $functionBodyScopeStart, 'MissingReturnStatement', $data ); 248 | } 249 | } 250 | 251 | /** 252 | * Is the current token inside a conditional? 253 | * 254 | * @param int $stackPtr The position in the stack where the token was found. 255 | * 256 | * @return bool 257 | */ 258 | private function isInsideIfConditonal( $stackPtr ) { 259 | 260 | // This check helps us in situations a class or a function is wrapped 261 | // inside a conditional as a whole. Eg.: inside `class_exists`. 262 | if ( end( $this->tokens[ $stackPtr ]['conditions'] ) === T_FUNCTION ) { 263 | return false; 264 | } 265 | 266 | // Similar case may be a conditional closure. 267 | if ( end( $this->tokens[ $stackPtr ]['conditions'] ) === T_CLOSURE ) { 268 | return false; 269 | } 270 | 271 | // Loop over the array of conditions and look for an IF. 272 | reset( $this->tokens[ $stackPtr ]['conditions'] ); 273 | 274 | if ( array_key_exists( 'conditions', $this->tokens[ $stackPtr ] ) === true 275 | && is_array( $this->tokens[ $stackPtr ]['conditions'] ) === true 276 | && empty( $this->tokens[ $stackPtr ]['conditions'] ) === false 277 | ) { 278 | foreach ( $this->tokens[ $stackPtr ]['conditions'] as $tokenPtr => $tokenCode ) { 279 | if ( $this->tokens[ $stackPtr ]['conditions'][ $tokenPtr ] === T_IF ) { 280 | return true; 281 | } 282 | } 283 | } 284 | return false; 285 | } 286 | 287 | /** 288 | * Is the token returning void 289 | * 290 | * @param int $stackPtr The position in the stack where the token was found. 291 | * 292 | * @return bool 293 | **/ 294 | private function isReturningVoid( $stackPtr ) { 295 | 296 | $nextToReturnTokenPtr = $this->phpcsFile->findNext( 297 | [ Tokens::$emptyTokens ], 298 | $stackPtr + 1, 299 | null, 300 | true 301 | ); 302 | 303 | return $this->tokens[ $nextToReturnTokenPtr ]['code'] === T_SEMICOLON; 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /WordPressVIPMinimum/Sniffs/Security/ProperEscapingFunctionSniff.php: -------------------------------------------------------------------------------- 1 | href|src|url|(^|\s+)action)?(?<=[a-z0-9_-])=(?:\\\\)?["\']*$`i'; 28 | 29 | /** 30 | * List of escaping functions which are being tested. 31 | * 32 | * @var array 33 | */ 34 | protected $escaping_functions = [ 35 | 'esc_url' => 'url', 36 | 'esc_attr' => 'attr', 37 | 'esc_attr__' => 'attr', 38 | 'esc_attr_x' => 'attr', 39 | 'esc_attr_e' => 'attr', 40 | 'esc_html' => 'html', 41 | 'esc_html__' => 'html', 42 | 'esc_html_x' => 'html', 43 | 'esc_html_e' => 'html', 44 | ]; 45 | 46 | /** 47 | * List of tokens we can skip. 48 | * 49 | * @var array 50 | */ 51 | private $echo_or_concat_tokens = 52 | [ 53 | T_ECHO => T_ECHO, 54 | T_OPEN_TAG => T_OPEN_TAG, 55 | T_OPEN_TAG_WITH_ECHO => T_OPEN_TAG_WITH_ECHO, 56 | T_STRING_CONCAT => T_STRING_CONCAT, 57 | T_NS_SEPARATOR => T_NS_SEPARATOR, 58 | ]; 59 | 60 | /** 61 | * List of attributes associated with url outputs. 62 | * 63 | * @deprecated 2.3.1 Currently unused by the sniff, but needed for 64 | * for public methods which extending sniffs may be 65 | * relying on. 66 | * 67 | * @var array 68 | */ 69 | private $url_attrs = [ 70 | 'href', 71 | 'src', 72 | 'url', 73 | 'action', 74 | ]; 75 | 76 | /** 77 | * List of syntaxes for inside attribute detection. 78 | * 79 | * @deprecated 2.3.1 Currently unused by the sniff, but needed for 80 | * for public methods which extending sniffs may be 81 | * relying on. 82 | * 83 | * @var array 84 | */ 85 | private $attr_endings = [ 86 | '=', 87 | '="', 88 | "='", 89 | "=\\'", 90 | '=\\"', 91 | ]; 92 | 93 | /** 94 | * Keep track of whether or not we're currently in the first statement of a short open echo tag. 95 | * 96 | * @var int|false Integer stack pointer to the end of the first statement in the current 97 | * short open echo tag or false when not in a short open echo tag. 98 | */ 99 | private $in_short_echo = false; 100 | 101 | /** 102 | * Keep track of the current file, so we can reset $in_short_echo for each new file. 103 | * 104 | * @var string Absolute file name of the file being processed. Defaults to an empty string. 105 | */ 106 | private $current_file = ''; 107 | 108 | /** 109 | * Returns an array of tokens this test wants to listen for. 110 | * 111 | * @return array 112 | */ 113 | public function register() { 114 | $this->echo_or_concat_tokens += Tokens::$emptyTokens; 115 | 116 | return [ 117 | T_STRING, 118 | T_OPEN_TAG_WITH_ECHO, 119 | ]; 120 | } 121 | 122 | /** 123 | * Process this test when one of its tokens is encountered 124 | * 125 | * @param int $stackPtr The position of the current token in the stack passed in $tokens. 126 | * 127 | * @return void 128 | */ 129 | public function process_token( $stackPtr ) { 130 | // Reset short echo context tracking variable for a new file. 131 | if ( $this->phpcsFile->getFilename() !== $this->current_file ) { 132 | $this->in_short_echo = false; 133 | $this->current_file = $this->phpcsFile->getFilename(); 134 | } 135 | 136 | /* 137 | * Short open echo tags will act as an echo for the first expression and 138 | * allow for passing multiple comma-separated parameters. 139 | * However, short open echo tags also allow for additional statements after, but 140 | * those have to be full PHP statements, not expressions. 141 | * 142 | * This snippet of code will keep track of whether or not we're in the first 143 | * expression in a short open echo tag. 144 | * $phpcsFile->findStartOfStatement() unfortunately is useless, as it will return 145 | * the first token in the statement, which can be anything - variable, text string - 146 | * without any indication of whether this is the start of a normal statement or 147 | * a short open echo expression. 148 | * So, if we used that, we'd need to walk back from every start of statement to 149 | * the previous non-empty to see if it is the short open echo tag. 150 | */ 151 | if ( $this->tokens[ $stackPtr ]['code'] === T_OPEN_TAG_WITH_ECHO ) { 152 | $end_of_echo = $this->phpcsFile->findNext( [ T_SEMICOLON, T_CLOSE_TAG ], ( $stackPtr + 1 ) ); 153 | if ( $end_of_echo === false ) { 154 | $this->in_short_echo = $this->phpcsFile->numTokens; 155 | } else { 156 | $this->in_short_echo = $end_of_echo; 157 | } 158 | 159 | return; 160 | } 161 | 162 | if ( $this->in_short_echo !== false && $this->in_short_echo < $stackPtr ) { 163 | $this->in_short_echo = false; 164 | } 165 | 166 | $function_name = strtolower( $this->tokens[ $stackPtr ]['content'] ); 167 | 168 | if ( isset( $this->escaping_functions[ $function_name ] ) === false ) { 169 | return; 170 | } 171 | 172 | $next_non_empty = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true ); 173 | if ( $next_non_empty === false || $this->tokens[ $next_non_empty ]['code'] !== T_OPEN_PARENTHESIS ) { 174 | // Not a function call. 175 | return; 176 | } 177 | 178 | $ignore = $this->echo_or_concat_tokens; 179 | if ( $this->in_short_echo !== false ) { 180 | $ignore[ T_COMMA ] = T_COMMA; 181 | } else { 182 | $start_of_statement = BCFile::findStartOfStatement( $this->phpcsFile, $stackPtr, T_COMMA ); 183 | if ( $this->tokens[ $start_of_statement ]['code'] === T_ECHO ) { 184 | $ignore[ T_COMMA ] = T_COMMA; 185 | } 186 | } 187 | 188 | $html = $this->phpcsFile->findPrevious( $ignore, $stackPtr - 1, null, true ); 189 | 190 | // Use $textStringTokens b/c heredoc and nowdoc tokens will never be encountered in this context anyways. 191 | if ( $html === false || isset( Tokens::$textStringTokens[ $this->tokens[ $html ]['code'] ] ) === false ) { 192 | return; 193 | } 194 | 195 | $data = [ $function_name ]; 196 | 197 | $content = $this->tokens[ $html ]['content']; 198 | if ( isset( Tokens::$stringTokens[ $this->tokens[ $html ]['code'] ] ) === true ) { 199 | $content = TextStrings::stripQuotes( $content ); 200 | } 201 | 202 | $escaping_type = $this->escaping_functions[ $function_name ]; 203 | 204 | if ( $escaping_type === 'attr' && $this->is_outside_html_attr_context( $content ) ) { 205 | $message = 'Wrong escaping function, using `%s()` in a context outside of HTML attributes may not escape properly.'; 206 | $this->phpcsFile->addError( $message, $html, 'notAttrEscAttr', $data ); 207 | return; 208 | } 209 | 210 | if ( preg_match( self::ATTR_END_REGEX, $content, $matches ) !== 1 ) { 211 | return; 212 | } 213 | 214 | if ( $escaping_type !== 'url' && empty( $matches['attrname'] ) === false ) { 215 | $message = 'Wrong escaping function. href, src, and action attributes should be escaped by `esc_url()`, not by `%s()`.'; 216 | $this->phpcsFile->addError( $message, $stackPtr, 'hrefSrcEscUrl', $data ); 217 | return; 218 | } 219 | 220 | if ( $escaping_type === 'html' ) { 221 | $message = 'Wrong escaping function. HTML attributes should be escaped by `esc_attr()`, not by `%s()`.'; 222 | $this->phpcsFile->addError( $message, $stackPtr, 'htmlAttrNotByEscHTML', $data ); 223 | return; 224 | } 225 | } 226 | 227 | /** 228 | * Tests whether provided string ends with open attribute which expects a URL value. 229 | * 230 | * @deprecated 2.3.1 231 | * 232 | * @param string $content Haystack in which we look for an open attribute which exects a URL value. 233 | * 234 | * @return bool True if string ends with open attribute which expects a URL value. 235 | */ 236 | public function attr_expects_url( $content ) { 237 | $attr_expects_url = false; 238 | foreach ( $this->url_attrs as $attr ) { 239 | foreach ( $this->attr_endings as $ending ) { 240 | if ( $this->endswith( $content, $attr . $ending ) === true ) { 241 | $attr_expects_url = true; 242 | break; 243 | } 244 | } 245 | } 246 | return $attr_expects_url; 247 | } 248 | 249 | /** 250 | * Tests whether provided string ends with open HMTL attribute. 251 | * 252 | * @deprecated 2.3.1 253 | * 254 | * @param string $content Haystack in which we look for open HTML attribute. 255 | * 256 | * @return bool True if string ends with open HTML attribute. 257 | */ 258 | public function is_html_attr( $content ) { 259 | $is_html_attr = false; 260 | foreach ( $this->attr_endings as $ending ) { 261 | if ( $this->endswith( $content, $ending ) === true ) { 262 | $is_html_attr = true; 263 | break; 264 | } 265 | } 266 | return $is_html_attr; 267 | } 268 | 269 | /** 270 | * Tests whether an attribute escaping function is being used outside of an HTML tag. 271 | * 272 | * @param string $content Haystack where we look for the end of a HTML tag. 273 | * 274 | * @return bool True if the passed string ends a HTML tag. 275 | */ 276 | public function is_outside_html_attr_context( $content ) { 277 | return $this->endswith( trim( $content ), '>' ); 278 | } 279 | 280 | /** 281 | * A helper function which tests whether string ends with some other. 282 | * 283 | * @param string $haystack String which is being tested. 284 | * @param string $needle The substring, which we try to locate on the end of the $haystack. 285 | * 286 | * @return bool True if haystack ends with needle. 287 | */ 288 | public function endswith( $haystack, $needle ) { 289 | return substr( $haystack, -strlen( $needle ) ) === $needle; 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /WordPressVIPMinimum/ruleset.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | WordPress VIP Minimum Coding Standards 4 | 5 | 12 | 13 | 20 | 21 | 22 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | warning 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | *\.php 72 | *\.inc 73 | *\.js 74 | *\.css 75 | 76 | 77 | 78 | 79 | 80 | error 81 | `eval()` is a security risk, please refrain from using it. 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | error 95 | 96 | 97 | error 98 | 99 | 100 | error 101 | 102 | 103 | error 104 | 105 | 106 | error 107 | 108 | 109 | error 110 | 111 | 112 | error 113 | 114 | 115 | error 116 | 117 | 118 | error 119 | 120 | 121 | error 122 | 123 | 124 | error 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | error 135 | 136 | 137 | error 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | Using cURL functions is highly discouraged within VIP context. Please see: https://docs.wpvip.com/technical-references/code-quality-and-best-practices/retrieving-remote-data/. 178 | 179 | 180 | Using cURL functions is highly discouraged within VIP context. Please see: https://docs.wpvip.com/technical-references/code-quality-and-best-practices/retrieving-remote-data/. 181 | 182 | 183 | Using cURL functions is highly discouraged within VIP context. Please see: https://docs.wpvip.com/technical-references/code-quality-and-best-practices/retrieving-remote-data/. 184 | 185 | 186 | 187 | error 188 | `%1$s()` performs a no-LIMIT query by default, make sure to set a reasonable `posts_per_page`. `%1$s()` will do a -1 query by default, a maximum of 100 should be used. 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | -------------------------------------------------------------------------------- /WordPressVIPMinimum/Sniffs/Functions/CheckReturnValueSniff.php: -------------------------------------------------------------------------------- 1 | 21 | * echo esc_url( wpcom_vip_get_term_link( $term ) ); 22 | * 23 | */ 24 | class CheckReturnValueSniff extends Sniff { 25 | 26 | /** 27 | * Pairs we are about to check. 28 | * 29 | * @var array> 30 | */ 31 | public $catch = [ 32 | 'esc_url' => [ 33 | 'get_term_link', 34 | ], 35 | 'wp_list_pluck' => [ 36 | 'get_the_tags', 37 | 'get_the_terms', 38 | ], 39 | 'foreach' => [ 40 | 'get_post_meta', 41 | 'get_term_meta', 42 | 'get_the_terms', 43 | 'get_the_tags', 44 | ], 45 | 'array_key_exists' => [ 46 | 'get_option', 47 | ], 48 | ]; 49 | 50 | /** 51 | * Tokens we are about to examine, which are not functions. 52 | * 53 | * @var array 54 | */ 55 | public $notFunctions = [ 56 | 'foreach' => T_FOREACH, 57 | ]; 58 | 59 | /** 60 | * Returns the token types that this sniff is interested in. 61 | * 62 | * @return array 63 | */ 64 | public function register() { 65 | return [ T_STRING ]; 66 | } 67 | 68 | /** 69 | * Processes the tokens that this sniff is interested in. 70 | * 71 | * @param int $stackPtr The position in the stack where 72 | * the token was found. 73 | * 74 | * @return void 75 | */ 76 | public function process_token( $stackPtr ) { 77 | 78 | $this->findDirectFunctionCalls( $stackPtr ); 79 | $this->findNonCheckedVariables( $stackPtr ); 80 | } 81 | 82 | /** 83 | * Check whether the currently examined code is a function call. 84 | * 85 | * @param int $stackPtr The position of the current token in the stack passed in $tokens. 86 | * 87 | * @return bool 88 | */ 89 | private function isFunctionCall( $stackPtr ) { 90 | 91 | if ( $this->tokens[ $stackPtr ]['code'] !== T_STRING ) { 92 | return false; 93 | } 94 | 95 | // Find the next non-empty token. 96 | $openBracket = $this->phpcsFile->findNext( Tokens::$emptyTokens, $stackPtr + 1, null, true ); 97 | 98 | if ( $this->tokens[ $openBracket ]['code'] !== T_OPEN_PARENTHESIS ) { 99 | // Not a function call. 100 | return false; 101 | } 102 | 103 | // Find the previous non-empty token. 104 | $search = Tokens::$emptyTokens; 105 | $search[] = T_BITWISE_AND; 106 | $previous = $this->phpcsFile->findPrevious( $search, $stackPtr - 1, null, true ); 107 | 108 | // It's a function definition, not a function call, so return false. 109 | return ! ( $this->tokens[ $previous ]['code'] === T_FUNCTION ); 110 | } 111 | 112 | /** 113 | * Check whether the examined code is a variable assignment. 114 | * 115 | * @param int $stackPtr The position of the current token in the stack passed in $tokens. 116 | * 117 | * @return int|false 118 | */ 119 | private function isVariableAssignment( $stackPtr ) { 120 | 121 | // Find the previous non-empty token. 122 | $search = Tokens::$emptyTokens; 123 | $search[] = T_BITWISE_AND; 124 | $previous = $this->phpcsFile->findPrevious( $search, $stackPtr - 1, null, true ); 125 | 126 | if ( $this->tokens[ $previous ]['code'] !== T_EQUAL ) { 127 | // It's not a variable assignment. 128 | return false; 129 | } 130 | 131 | $previous = $this->phpcsFile->findPrevious( $search, $previous - 1, null, true ); 132 | 133 | if ( $this->tokens[ $previous ]['code'] !== T_VARIABLE ) { 134 | // It's not a variable assignment. 135 | return false; 136 | } 137 | 138 | return $previous; 139 | } 140 | 141 | /** 142 | * Find instances in which a function call is directly passed to another one w/o checking the return type 143 | * 144 | * @param int $stackPtr The position of the current token in the stack passed in $tokens. 145 | * 146 | * @return void 147 | */ 148 | public function findDirectFunctionCalls( $stackPtr ) { 149 | 150 | $functionName = $this->tokens[ $stackPtr ]['content']; 151 | 152 | if ( array_key_exists( $functionName, $this->catch ) === false ) { 153 | // Not a function we are looking for. 154 | return; 155 | } 156 | 157 | if ( $this->isFunctionCall( $stackPtr ) === false ) { 158 | // Not a function call. 159 | return; 160 | } 161 | 162 | // Find the next non-empty token. 163 | $openBracket = $this->phpcsFile->findNext( Tokens::$emptyTokens, $stackPtr + 1, null, true ); 164 | 165 | // Find the closing bracket. 166 | $closeBracket = $this->tokens[ $openBracket ]['parenthesis_closer']; 167 | 168 | $startNext = $openBracket + 1; 169 | $next = $this->phpcsFile->findNext( T_STRING, $startNext, $closeBracket, false, null, true ); 170 | while ( $next ) { 171 | if ( in_array( $this->tokens[ $next ]['content'], $this->catch[ $functionName ], true ) === true ) { 172 | $message = "`%s`'s return type must be checked before calling `%s` using that value."; 173 | $data = [ $this->tokens[ $next ]['content'], $functionName ]; 174 | $this->phpcsFile->addError( $message, $next, 'DirectFunctionCall', $data ); 175 | } 176 | $next = $this->phpcsFile->findNext( T_STRING, $next + 1, $closeBracket, false, null, true ); 177 | } 178 | } 179 | 180 | /** 181 | * Deals with situations in which the variable is being used later in the code along with a function which is known for causing issues. 182 | * 183 | * This only catches situations in which the variable is not being used with some other function before it's interacting with function we look for. 184 | * That's currently necessary in order to prevent false positives. 185 | * 186 | * @param int $stackPtr The position of the current token in the stack passed in $tokens. 187 | * 188 | * @return void 189 | */ 190 | public function findNonCheckedVariables( $stackPtr ) { 191 | 192 | $functionName = $this->tokens[ $stackPtr ]['content']; 193 | 194 | $isFunctionWeLookFor = false; 195 | 196 | $callees = []; 197 | 198 | foreach ( $this->catch as $callee => $checkReturnArray ) { 199 | if ( in_array( $functionName, $checkReturnArray, true ) === true ) { 200 | $isFunctionWeLookFor = true; 201 | $callees[] = $callee; 202 | } 203 | } 204 | 205 | if ( $isFunctionWeLookFor === false ) { 206 | // Not a function we are looking for. 207 | return; 208 | } 209 | 210 | if ( $this->isFunctionCall( $stackPtr ) === false ) { 211 | // Not a function call. 212 | return; 213 | } 214 | 215 | $variablePos = $this->isVariableAssignment( $stackPtr ); 216 | 217 | if ( $variablePos === false ) { 218 | // Not a variable assignment. 219 | return; 220 | } 221 | 222 | $variableToken = $this->tokens[ $variablePos ]; 223 | $variableName = $variableToken['content']; 224 | 225 | // Find the next non-empty token. 226 | $openBracket = $this->phpcsFile->findNext( Tokens::$emptyTokens, $stackPtr + 1, null, true ); 227 | 228 | // Find the closing bracket. 229 | $closeBracket = $this->tokens[ $openBracket ]['parenthesis_closer']; 230 | 231 | if ( in_array( $functionName, [ 'get_post_meta', 'get_term_meta' ], true ) === true ) { 232 | // Since the get_post_meta and get_term_meta always returns an array if $single is set to `true` we need to check for the value of it's third param before proceeding. 233 | $params = []; 234 | $paramNo = 1; 235 | $prevCommaPos = $openBracket + 1; 236 | 237 | for ( $i = $openBracket + 1; $i <= $closeBracket; $i++ ) { 238 | 239 | if ( $this->tokens[ $i ]['code'] === T_OPEN_PARENTHESIS ) { 240 | $i = $this->tokens[ $i ]['parenthesis_closer']; 241 | } 242 | 243 | if ( $this->tokens[ $i ]['code'] === T_COMMA ) { 244 | $params[ $paramNo++ ] = trim( array_reduce( array_slice( $this->tokens, $prevCommaPos, $i - $prevCommaPos ), [ $this, 'reduce_array' ] ) ); 245 | $prevCommaPos = $i + 1; 246 | } 247 | 248 | if ( $i === $closeBracket ) { 249 | $params[ $paramNo ] = trim( array_reduce( array_slice( $this->tokens, $prevCommaPos, $i - $prevCommaPos ), [ $this, 'reduce_array' ] ) ); 250 | break; 251 | } 252 | } 253 | 254 | if ( array_key_exists( 3, $params ) === false || $params[3] === 'false' ) { 255 | // Third param of get_post_meta is not set (default to false) or is set to false. 256 | // Means the function returns an array. We are good then. 257 | return; 258 | } 259 | } 260 | 261 | $nextVariableOccurrence = $this->phpcsFile->findNext( T_VARIABLE, $closeBracket + 1, null, false, $variableName ); 262 | 263 | // Find previous non-empty token, which is not an open parenthesis, comma nor variable. 264 | $search = Tokens::$emptyTokens; 265 | $search[] = T_OPEN_PARENTHESIS; 266 | // This allows us to check for variables which are passed as second parameter of a function e.g.: array_key_exists. 267 | $search[] = T_COMMA; 268 | $search[] = T_VARIABLE; 269 | $search[] = T_CONSTANT_ENCAPSED_STRING; 270 | 271 | $nextFunctionCallWithVariable = $this->phpcsFile->findPrevious( $search, $nextVariableOccurrence - 1, null, true ); 272 | 273 | foreach ( $callees as $callee ) { 274 | $notFunctionsCallee = array_key_exists( $callee, $this->notFunctions ) ? (array) $this->notFunctions[ $callee ] : []; 275 | // Check whether the found token is one of the function calls (or foreach call) we are interested in. 276 | if ( in_array( $this->tokens[ $nextFunctionCallWithVariable ]['code'], array_merge( [ T_STRING ], $notFunctionsCallee ), true ) === true 277 | && $this->tokens[ $nextFunctionCallWithVariable ]['content'] === $callee 278 | ) { 279 | $this->addNonCheckedVariableError( $nextFunctionCallWithVariable, $variableName, $callee ); 280 | return; 281 | } 282 | 283 | $search = array_merge( Tokens::$emptyTokens, [ T_EQUAL ] ); 284 | $next = $this->phpcsFile->findNext( $search, $nextVariableOccurrence + 1, null, true ); 285 | if ( $this->tokens[ $next ]['code'] === T_STRING 286 | && $this->tokens[ $next ]['content'] === $callee 287 | ) { 288 | $this->addNonCheckedVariableError( $next, $variableName, $callee ); 289 | return; 290 | } 291 | } 292 | } 293 | 294 | /** 295 | * Function used as as callback for the array_reduce call. 296 | * 297 | * @param string|null $carry The final string. 298 | * @param array $item Processed item. 299 | * 300 | * @return string 301 | */ 302 | public function reduce_array( $carry, $item ) { 303 | return $carry . $item['content']; 304 | } 305 | 306 | /** 307 | * Consolidated violation. 308 | * 309 | * @param int $stackPtr The position in the stack where the token was found. 310 | * @param string $variableName Variable name. 311 | * @param string $callee Function name. 312 | * 313 | * @return void 314 | */ 315 | private function addNonCheckedVariableError( $stackPtr, $variableName, $callee ) { 316 | $message = 'Type of `%s` must be checked before calling `%s()` using that variable.'; 317 | $data = [ $variableName, $callee ]; 318 | $this->phpcsFile->addError( $message, $stackPtr, 'NonCheckedVariable', $data ); 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /WordPressVIPMinimum/Sniffs/Functions/RestrictedFunctionsSniff.php: -------------------------------------------------------------------------------- 1 | |array>> 24 | */ 25 | public function getGroups() { 26 | 27 | $groups = [ 28 | 'opcache' => [ 29 | 'type' => 'error', 30 | 'message' => '`%s` is prohibited on the WordPress VIP platform due to memory corruption.', 31 | 'functions' => [ 32 | 'opcache_reset', 33 | 'opcache_invalidate', 34 | 'opcache_compile_file', 35 | ], 36 | ], 37 | 'config_settings' => [ 38 | 'type' => 'error', 39 | 'message' => '`%s` is not recommended for use on the WordPress VIP platform due to potential setting changes.', 40 | 'functions' => [ 41 | 'opcache_is_script_cached', 42 | 'opcache_get_status', 43 | 'opcache_get_configuration', 44 | ], 45 | ], 46 | 'internal' => [ 47 | 'type' => 'error', 48 | 'message' => '`%1$s()` is for internal use only.', 49 | 'functions' => [ 50 | 'wpcom_vip_irc', 51 | ], 52 | ], 53 | 'flush_rewrite_rules' => [ 54 | 'type' => 'error', 55 | 'message' => '`%s` should not be used in any normal circumstances in the theme code.', 56 | 'functions' => [ 57 | 'flush_rewrite_rules', 58 | ], 59 | ], 60 | 'flush_rules' => [ 61 | 'type' => 'error', 62 | 'message' => '`%s` should not be used in any normal circumstances in the theme code.', 63 | 'functions' => [ 64 | 'flush_rules', 65 | ], 66 | 'object_var' => [ 67 | '$wp_rewrite' => true, 68 | ], 69 | ], 70 | 'attachment_url_to_postid' => [ 71 | 'type' => 'error', 72 | 'message' => '`%s()` is prohibited, please use `wpcom_vip_attachment_url_to_postid()` instead.', 73 | 'functions' => [ 74 | 'attachment_url_to_postid', 75 | ], 76 | ], 77 | // @link https://docs.wpvip.com/technical-references/code-review/vip-notices/#h-switch_to_blog 78 | 'switch_to_blog' => [ 79 | 'type' => 'warning', 80 | 'message' => '%s() may not work as expected since it only changes the database context for the blog and does not load the plugins or theme of that site. Filters or hooks on the blog you are switching to will not run.', 81 | 'functions' => [ 82 | 'switch_to_blog', 83 | ], 84 | ], 85 | 'url_to_postid' => [ 86 | 'type' => 'error', 87 | 'message' => '%s() is prohibited, please use wpcom_vip_url_to_postid() instead.', 88 | 'functions' => [ 89 | 'url_to_postid', 90 | ], 91 | ], 92 | // @link https://docs.wpvip.com/how-tos/customize-user-roles/ 93 | 'custom_role' => [ 94 | 'type' => 'error', 95 | 'message' => 'Use wpcom_vip_add_role() instead of %s().', 96 | 'functions' => [ 97 | 'add_role', 98 | ], 99 | ], 100 | 'count_user_posts' => [ 101 | 'type' => 'error', 102 | 'message' => '%s() is highly discouraged due to not being cached; please use wpcom_vip_count_user_posts() instead.', 103 | 'functions' => [ 104 | 'count_user_posts', 105 | ], 106 | ], 107 | 'wp_old_slug_redirect' => [ 108 | 'type' => 'error', 109 | 'message' => '%s() is highly discouraged due to not being cached; please use wpcom_vip_old_slug_redirect() instead.', 110 | 'functions' => [ 111 | 'wp_old_slug_redirect', 112 | ], 113 | ], 114 | 'get_adjacent_post' => [ 115 | 'type' => 'error', 116 | 'message' => '%s() is highly discouraged due to not being cached; please use wpcom_vip_get_adjacent_post() instead.', 117 | 'functions' => [ 118 | 'get_adjacent_post', 119 | 'get_previous_post', 120 | 'get_previous_post_link', 121 | 'get_next_post', 122 | 'get_next_post_link', 123 | ], 124 | ], 125 | 'get_intermediate_image_sizes' => [ 126 | 'type' => 'error', 127 | 'message' => 'Intermediate images do not exist on the VIP platform, and thus get_intermediate_image_sizes() returns an empty array() on the platform. This behavior is intentional to prevent WordPress from generating multiple thumbnails when images are uploaded.', 128 | 'functions' => [ 129 | 'get_intermediate_image_sizes', 130 | ], 131 | ], 132 | // @link https://docs.wpvip.com/technical-references/code-review/vip-warnings/#h-mobile-detection 133 | 'wp_is_mobile' => [ 134 | 'type' => 'error', 135 | 'message' => '%s() found. When targeting mobile visitors, jetpack_is_mobile() should be used instead of wp_is_mobile. It is more robust and works better with full page caching.', 136 | 'functions' => [ 137 | 'wp_is_mobile', 138 | ], 139 | ], 140 | 'session' => [ 141 | 'type' => 'error', 142 | 'message' => 'The use of PHP session function %s() is prohibited.', 143 | 'functions' => [ 144 | 'session_abort', 145 | 'session_cache_expire', 146 | 'session_cache_limiter', 147 | 'session_commit', 148 | 'session_create_id', 149 | 'session_decode', 150 | 'session_destroy', 151 | 'session_encode', 152 | 'session_gc', 153 | 'session_get_cookie_params', 154 | 'session_id', 155 | 'session_is_registered', 156 | 'session_module_name', 157 | 'session_name', 158 | 'session_regenerate_id', 159 | 'session_register_shutdown', 160 | 'session_register', 161 | 'session_reset', 162 | 'session_save_path', 163 | 'session_set_cookie_params', 164 | 'session_set_save_handler', 165 | 'session_start', 166 | 'session_status', 167 | 'session_unregister', 168 | 'session_unset', 169 | 'session_write_close', 170 | ], 171 | ], 172 | 'file_ops' => [ 173 | 'type' => 'error', 174 | 'message' => 'Filesystem writes are forbidden, please do not use %s().', 175 | 'functions' => [ 176 | 'file_put_contents', 177 | 'flock', 178 | 'fputcsv', 179 | 'fputs', 180 | 'fwrite', 181 | 'ftruncate', 182 | 'is_writable', 183 | 'is_writeable', 184 | 'link', 185 | 'rename', 186 | 'symlink', 187 | 'tempnam', 188 | 'touch', 189 | 'unlink', 190 | ], 191 | ], 192 | 'directory' => [ 193 | 'type' => 'error', 194 | 'message' => 'Filesystem writes are forbidden, please do not use %s().', 195 | 'functions' => [ 196 | 'mkdir', 197 | 'rmdir', 198 | ], 199 | ], 200 | 'chmod' => [ 201 | 'type' => 'error', 202 | 'message' => 'Filesystem writes are forbidden, please do not use %s().', 203 | 'functions' => [ 204 | 'chgrp', 205 | 'chown', 206 | 'chmod', 207 | 'lchgrp', 208 | 'lchown', 209 | ], 210 | ], 211 | 'stats_get_csv' => [ 212 | 'type' => 'error', 213 | 'message' => 'Using `%s` outside of Jetpack context pollutes the stats_cache entry in the wp_options table. We recommend building a custom function instead.', 214 | 'functions' => [ 215 | 'stats_get_csv', 216 | ], 217 | ], 218 | 'wp_mail' => [ 219 | 'type' => 'warning', 220 | 'message' => '`%s` should be used sparingly. For any bulk emailing should be handled by a 3rd party service, in order to prevent domain or IP addresses being flagged as spam.', 221 | 'functions' => [ 222 | 'wp_mail', 223 | 'mail', 224 | ], 225 | ], 226 | 'is_multi_author' => [ 227 | 'type' => 'warning', 228 | 'message' => '`%s` can be very slow on large sites and likely not needed on many VIP sites since they tend to have more than one author.', 229 | 'functions' => [ 230 | 'is_multi_author', 231 | ], 232 | ], 233 | 'advanced_custom_fields' => [ 234 | 'type' => 'warning', 235 | 'message' => '`%1$s` does not escape output by default, please echo and escape with the `get_*()` variant function instead (i.e. `get_field()`).', 236 | 'functions' => [ 237 | 'the_sub_field', 238 | 'the_field', 239 | ], 240 | ], 241 | // @link https://docs.wpvip.com/technical-references/code-review/vip-warnings/#h-remote-calls 242 | 'wp_remote_get' => [ 243 | 'type' => 'warning', 244 | 'message' => '%s() is highly discouraged. Please use vip_safe_wp_remote_get() instead which is designed to more gracefully handle failure than wp_remote_get() does.', 245 | 'functions' => [ 246 | 'wp_remote_get', 247 | ], 248 | ], 249 | // @link https://docs.wpvip.com/technical-references/code-review/vip-errors/#h-cache-constraints 250 | 'cookies' => [ 251 | 'type' => 'error', 252 | 'message' => 'Due to server-side caching, server-side based client related logic might not work. We recommend implementing client side logic in JavaScript instead.', 253 | 'functions' => [ 254 | 'setcookie', 255 | ], 256 | ], 257 | // @todo Introduce a sniff specific to get_posts() that checks for suppress_filters=>false being supplied. 258 | 'get_posts' => [ 259 | 'type' => 'warning', 260 | 'message' => '%s() is uncached unless the "suppress_filters" parameter is set to false. If the suppress_filter parameter is set to false this can be safely ignored. More Info: https://docs.wpvip.com/technical-references/caching/uncached-functions/.', 261 | 'functions' => [ 262 | 'get_posts', 263 | 'wp_get_recent_posts', 264 | 'get_children', 265 | ], 266 | ], 267 | 'create_function' => [ 268 | 'type' => 'warning', 269 | 'message' => '%s() is highly discouraged, as it can execute arbritary code (additionally, it\'s deprecated as of PHP 7.2): https://docs.wpvip.com/technical-references/code-review/vip-warnings/#h-eval-and-create_function. )', 270 | 'functions' => [ 271 | 'create_function', 272 | ], 273 | ], 274 | ]; 275 | 276 | $deprecated_vip_helpers = [ 277 | 'get_term_link' => 'wpcom_vip_get_term_link', 278 | 'get_term_by' => 'wpcom_vip_get_term_by', 279 | 'get_category_by_slug' => 'wpcom_vip_get_category_by_slug', 280 | ]; 281 | foreach ( $deprecated_vip_helpers as $restricted => $helper ) { 282 | $groups[ $helper ] = [ 283 | 'type' => 'warning', 284 | 'message' => "`%s()` is deprecated, please use `{$restricted}()` instead.", 285 | 'functions' => [ 286 | $helper, 287 | ], 288 | ]; 289 | } 290 | 291 | return $groups; 292 | } 293 | 294 | /** 295 | * Verify the current token is a function call or a method call on a specific object variable. 296 | * 297 | * This differs to the parent class method that it overrides, by also checking to see if the 298 | * function call is actually a method call on a specific object variable. This works best with global objects, 299 | * such as the `flush_rules()` method on the `$wp_rewrite` object. 300 | * 301 | * @param int $stackPtr The position of the current token in the stack. 302 | * 303 | * @return bool 304 | */ 305 | public function is_targetted_token( $stackPtr ) { 306 | if ( empty( $this->groups[ $this->tokens[ $stackPtr ]['content'] ]['object_var'] ) ) { 307 | return parent::is_targetted_token( $stackPtr ); 308 | } 309 | 310 | // Start difference to parent class method. 311 | // Check to see if the token is a method call on a specific object variable. 312 | $next = $this->phpcsFile->findNext( Tokens::$emptyTokens, $stackPtr + 1, null, true ); 313 | if ( $next === false || $this->tokens[ $next ]['code'] !== T_OPEN_PARENTHESIS ) { 314 | return false; 315 | } 316 | 317 | $prev = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, $stackPtr - 1, null, true ); 318 | if ( $this->tokens[ $prev ]['code'] !== T_OBJECT_OPERATOR 319 | && $this->tokens[ $prev ]['code'] !== T_NULLSAFE_OBJECT_OPERATOR 320 | ) { 321 | return false; 322 | } 323 | 324 | $prevPrev = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, $prev - 1, null, true ); 325 | 326 | return $this->tokens[ $prevPrev ]['code'] === T_VARIABLE 327 | && isset( $this->groups[ $this->tokens[ $stackPtr ]['content'] ]['object_var'][ $this->tokens[ $prevPrev ]['content'] ] ); 328 | } 329 | } 330 | -------------------------------------------------------------------------------- /WordPress-VIP-Go/ruleset-test.php: -------------------------------------------------------------------------------- 1 | [ 18 | 50 => 1, 19 | 53 => 1, 20 | 56 => 1, 21 | 72 => 1, 22 | 83 => 1, 23 | 165 => 1, 24 | 180 => 1, 25 | 181 => 1, 26 | 187 => 1, 27 | 188 => 1, 28 | 252 => 1, 29 | 255 => 1, 30 | 256 => 1, 31 | 258 => 1, 32 | 259 => 1, 33 | 318 => 1, 34 | 329 => 1, 35 | 334 => 1, 36 | 337 => 1, 37 | 341 => 1, 38 | 342 => 1, 39 | 350 => 1, 40 | 351 => 1, 41 | 352 => 1, 42 | 353 => 1, 43 | 354 => 1, 44 | 355 => 1, 45 | 357 => 1, 46 | 358 => 1, 47 | 359 => 1, 48 | 360 => 1, 49 | 362 => 1, 50 | 363 => 1, 51 | 364 => 1, 52 | 365 => 1, 53 | 366 => 1, 54 | 367 => 1, 55 | 368 => 1, 56 | 369 => 1, 57 | 370 => 1, 58 | 371 => 1, 59 | 372 => 1, 60 | 373 => 1, 61 | 374 => 1, 62 | 375 => 1, 63 | 376 => 1, 64 | 377 => 1, 65 | 378 => 1, 66 | 379 => 1, 67 | 380 => 1, 68 | 381 => 1, 69 | 382 => 1, 70 | 383 => 1, 71 | 384 => 1, 72 | 385 => 1, 73 | 386 => 1, 74 | 387 => 1, 75 | 388 => 1, 76 | 389 => 1, 77 | 390 => 1, 78 | 409 => 1, 79 | 410 => 1, 80 | 411 => 1, 81 | 412 => 1, 82 | 413 => 1, 83 | 414 => 1, 84 | 415 => 1, 85 | 431 => 1, 86 | 441 => 1, 87 | 462 => 1, 88 | 466 => 1, 89 | 468 => 1, 90 | 472 => 1, 91 | 474 => 1, 92 | 480 => 1, 93 | 486 => 1, 94 | 494 => 1, 95 | 507 => 1, 96 | 511 => 1, 97 | 512 => 1, 98 | 513 => 1, 99 | 514 => 1, 100 | 515 => 1, 101 | 516 => 1, 102 | 517 => 1, 103 | 518 => 1, 104 | 519 => 1, 105 | 520 => 1, 106 | 521 => 1, 107 | 525 => 1, 108 | 527 => 1, 109 | 545 => 1, 110 | 560 => 1, 111 | 564 => 1, 112 | 565 => 1, 113 | 566 => 1, 114 | 567 => 1, 115 | 572 => 1, 116 | 574 => 1, 117 | ], 118 | 'warnings' => [ 119 | 7 => 1, 120 | 10 => 1, 121 | 14 => 1, 122 | 17 => 1, 123 | 20 => 1, 124 | 23 => 1, 125 | 26 => 1, 126 | 29 => 1, 127 | 32 => 1, 128 | 35 => 1, 129 | 38 => 1, 130 | 41 => 1, 131 | 44 => 1, 132 | 47 => 1, 133 | 63 => 1, 134 | 66 => 1, 135 | 83 => 1, 136 | 85 => 1, 137 | 90 => 1, 138 | 94 => 1, 139 | 95 => 1, 140 | 99 => 1, 141 | 102 => 1, 142 | 103 => 1, 143 | 104 => 1, 144 | 106 => 1, 145 | 108 => 1, 146 | 109 => 1, 147 | 112 => 1, 148 | 118 => 1, 149 | 119 => 1, 150 | 123 => 1, 151 | 127 => 1, 152 | 128 => 1, 153 | 129 => 1, 154 | 130 => 1, 155 | 131 => 1, 156 | 139 => 1, 157 | 142 => 1, 158 | 146 => 1, 159 | 150 => 1, 160 | 154 => 1, 161 | 157 => 1, 162 | 161 => 1, 163 | 169 => 1, 164 | 174 => 1, 165 | 175 => 1, 166 | 176 => 1, 167 | 177 => 1, 168 | 191 => 1, 169 | 192 => 1, 170 | 195 => 1, 171 | 196 => 1, 172 | 199 => 1, 173 | 200 => 1, 174 | 201 => 1, 175 | 204 => 1, 176 | 205 => 1, 177 | 206 => 1, 178 | 207 => 1, 179 | 208 => 1, 180 | 212 => 1, 181 | 221 => 1, 182 | 223 => 1, 183 | 225 => 1, 184 | 228 => 1, 185 | 229 => 1, 186 | 230 => 1, 187 | 235 => 1, 188 | 236 => 1, 189 | 237 => 1, 190 | 245 => 1, 191 | 246 => 1, 192 | 247 => 1, 193 | 265 => 1, 194 | 269 => 1, 195 | 273 => 1, 196 | 322 => 1, 197 | 332 => 1, 198 | 392 => 1, 199 | 394 => 1, 200 | 395 => 1, 201 | 398 => 1, 202 | 399 => 1, 203 | 400 => 1, 204 | 401 => 1, 205 | 402 => 1, 206 | 403 => 1, 207 | 404 => 1, 208 | 405 => 1, 209 | 406 => 1, 210 | 407 => 1, 211 | 408 => 1, 212 | 416 => 1, 213 | 417 => 1, 214 | 418 => 1, 215 | 419 => 1, 216 | 421 => 1, 217 | 423 => 1, 218 | 424 => 1, 219 | 425 => 1, 220 | 428 => 1, 221 | 448 => 1, 222 | 453 => 1, 223 | 454 => 1, 224 | 455 => 1, 225 | 456 => 1, 226 | 502 => 1, 227 | 503 => 1, 228 | 530 => 1, 229 | 533 => 1, 230 | 540 => 1, 231 | 550 => 1, 232 | 556 => 1, 233 | 579 => 1, 234 | ], 235 | 'messages' => [ 236 | 7 => [ 237 | 'File system operations only work on the `/tmp/` and `wp-content/uploads/` directories. To avoid unexpected results, please use helper functions like `get_temp_dir()` or `wp_get_upload_dir()` to get the proper directory path when using functions such as file_put_contents(). For more details, please see: https://docs.wpvip.com/technical-references/vip-go-files-system/local-file-operations/', 238 | ], 239 | 10 => [ 240 | 'File system operations only work on the `/tmp/` and `wp-content/uploads/` directories. To avoid unexpected results, please use helper functions like `get_temp_dir()` or `wp_get_upload_dir()` to get the proper directory path when using functions such as flock(). For more details, please see: https://docs.wpvip.com/technical-references/vip-go-files-system/local-file-operations/', 241 | ], 242 | 14 => [ 243 | 'File system operations only work on the `/tmp/` and `wp-content/uploads/` directories. To avoid unexpected results, please use helper functions like `get_temp_dir()` or `wp_get_upload_dir()` to get the proper directory path when using functions such as fputcsv(). For more details, please see: https://docs.wpvip.com/technical-references/vip-go-files-system/local-file-operations/', 244 | ], 245 | 17 => [ 246 | 'File system operations only work on the `/tmp/` and `wp-content/uploads/` directories. To avoid unexpected results, please use helper functions like `get_temp_dir()` or `wp_get_upload_dir()` to get the proper directory path when using functions such as fputs(). For more details, please see: https://docs.wpvip.com/technical-references/vip-go-files-system/local-file-operations/', 247 | ], 248 | 20 => [ 249 | 'File system operations only work on the `/tmp/` and `wp-content/uploads/` directories. To avoid unexpected results, please use helper functions like `get_temp_dir()` or `wp_get_upload_dir()` to get the proper directory path when using functions such as fwrite(). For more details, please see: https://docs.wpvip.com/technical-references/vip-go-files-system/local-file-operations/', 250 | ], 251 | 23 => [ 252 | 'File system operations only work on the `/tmp/` and `wp-content/uploads/` directories. To avoid unexpected results, please use helper functions like `get_temp_dir()` or `wp_get_upload_dir()` to get the proper directory path when using functions such as ftruncate(). For more details, please see: https://docs.wpvip.com/technical-references/vip-go-files-system/local-file-operations/', 253 | ], 254 | 26 => [ 255 | 'File system operations only work on the `/tmp/` and `wp-content/uploads/` directories. To avoid unexpected results, please use helper functions like `get_temp_dir()` or `wp_get_upload_dir()` to get the proper directory path when using functions such as is_writable(). For more details, please see: https://docs.wpvip.com/technical-references/vip-go-files-system/local-file-operations/', 256 | ], 257 | 29 => [ 258 | 'File system operations only work on the `/tmp/` and `wp-content/uploads/` directories. To avoid unexpected results, please use helper functions like `get_temp_dir()` or `wp_get_upload_dir()` to get the proper directory path when using functions such as is_writeable(). For more details, please see: https://docs.wpvip.com/technical-references/vip-go-files-system/local-file-operations/', 259 | ], 260 | 32 => [ 261 | 'File system operations only work on the `/tmp/` and `wp-content/uploads/` directories. To avoid unexpected results, please use helper functions like `get_temp_dir()` or `wp_get_upload_dir()` to get the proper directory path when using functions such as link(). For more details, please see: https://docs.wpvip.com/technical-references/vip-go-files-system/local-file-operations/', 262 | ], 263 | 35 => [ 264 | 'File system operations only work on the `/tmp/` and `wp-content/uploads/` directories. To avoid unexpected results, please use helper functions like `get_temp_dir()` or `wp_get_upload_dir()` to get the proper directory path when using functions such as rename(). For more details, please see: https://docs.wpvip.com/technical-references/vip-go-files-system/local-file-operations/', 265 | ], 266 | 38 => [ 267 | 'File system operations only work on the `/tmp/` and `wp-content/uploads/` directories. To avoid unexpected results, please use helper functions like `get_temp_dir()` or `wp_get_upload_dir()` to get the proper directory path when using functions such as symlink(). For more details, please see: https://docs.wpvip.com/technical-references/vip-go-files-system/local-file-operations/', 268 | ], 269 | 41 => [ 270 | 'File system operations only work on the `/tmp/` and `wp-content/uploads/` directories. To avoid unexpected results, please use helper functions like `get_temp_dir()` or `wp_get_upload_dir()` to get the proper directory path when using functions such as tempnam(). For more details, please see: https://docs.wpvip.com/technical-references/vip-go-files-system/local-file-operations/', 271 | ], 272 | 44 => [ 273 | 'File system operations only work on the `/tmp/` and `wp-content/uploads/` directories. To avoid unexpected results, please use helper functions like `get_temp_dir()` or `wp_get_upload_dir()` to get the proper directory path when using functions such as touch(). For more details, please see: https://docs.wpvip.com/technical-references/vip-go-files-system/local-file-operations/', 274 | ], 275 | 47 => [ 276 | 'File system operations only work on the `/tmp/` and `wp-content/uploads/` directories. To avoid unexpected results, please use helper functions like `get_temp_dir()` or `wp_get_upload_dir()` to get the proper directory path when using functions such as unlink(). For more details, please see: https://docs.wpvip.com/technical-references/vip-go-files-system/local-file-operations/', 277 | ], 278 | 50 => [ 279 | 'Due to server-side caching, server-side based client related logic might not work. We recommend implementing client side logic in JavaScript instead.', 280 | ], 281 | 53 => [ 282 | 'Due to server-side caching, server-side based client related logic might not work. We recommend implementing client side logic in JavaScript instead.', 283 | ], 284 | 56 => [ 285 | 'Due to server-side caching, server-side based client related logic might not work. We recommend implementing client side logic in JavaScript instead.', 286 | ], 287 | 63 => [ 288 | 'File system operations only work on the `/tmp/` and `wp-content/uploads/` directories. To avoid unexpected results, please use helper functions like `get_temp_dir()` or `wp_get_upload_dir()` to get the proper directory path when using functions such as fopen(). For more details, please see: https://docs.wpvip.com/technical-references/vip-go-files-system/local-file-operations/', 289 | ], 290 | 66 => [ 291 | 'file_get_contents() is uncached. If the function is being used to fetch a remote file (e.g. a URL starting with https://), please use wpcom_vip_file_get_contents() to ensure the results are cached. For more details, please see: https://docs.wpvip.com/technical-references/code-quality-and-best-practices/retrieving-remote-data/', 292 | ], 293 | 90 => [ 294 | 'Having more than 100 posts returned per page may lead to severe performance problems.', 295 | ], 296 | 94 => [ 297 | 'Having more than 100 posts returned per page may lead to severe performance problems.', 298 | ], 299 | 95 => [ 300 | 'Having more than 100 posts returned per page may lead to severe performance problems.', 301 | ], 302 | 123 => [ 303 | 'attachment_url_to_postid() is uncached, please use wpcom_vip_attachment_url_to_postid() instead.', 304 | ], 305 | 139 => [ 306 | 'get_children() is uncached and performs a no limit query. Please use get_posts or WP_Query instead. Please see: https://docs.wpvip.com/technical-references/caching/uncached-functions/', 307 | ], 308 | 150 => [ 309 | 'url_to_postid() is uncached, please use wpcom_vip_url_to_postid() instead.', 310 | ], 311 | 191 => [ 312 | 'Scripts should be registered/enqueued via `wp_enqueue_script`. This can improve the site\'s performance due to script concatenation.', 313 | ], 314 | 192 => [ 315 | 'Scripts should be registered/enqueued via `wp_enqueue_script`. This can improve the site\'s performance due to script concatenation.', 316 | ], 317 | 195 => [ 318 | 'Stylesheets should be registered/enqueued via `wp_enqueue_style`. This can improve the site\'s performance due to styles concatenation.', 319 | ], 320 | 196 => [ 321 | 'Stylesheets should be registered/enqueued via `wp_enqueue_style`. This can improve the site\'s performance due to styles concatenation.', 322 | ], 323 | ], 324 | ]; 325 | 326 | require __DIR__ . '/../tests/RulesetTest.php'; 327 | 328 | // Run the tests! 329 | $test = new RulesetTest( 'WordPress-VIP-Go', $expected ); 330 | if ( $test->passes() ) { 331 | printf( 'All WordPress-VIP-Go tests passed!' . PHP_EOL ); 332 | exit( 0 ); 333 | } 334 | 335 | exit( 1 ); 336 | --------------------------------------------------------------------------------