|
10 | 10 | namespace WordPressVIPMinimum\Sniffs\Performance; |
11 | 11 |
|
12 | 12 | use PHP_CodeSniffer\Util\Tokens; |
13 | | -use WordPressVIPMinimum\Sniffs\Sniff; |
| 13 | +use PHPCSUtils\Utils\PassedParameters; |
| 14 | +use WordPressCS\WordPress\AbstractFunctionParameterSniff; |
14 | 15 |
|
15 | 16 | /** |
16 | 17 | * Restricts usage of file_get_contents(). |
17 | 18 | */ |
18 | | -class FetchingRemoteDataSniff extends Sniff { |
| 19 | +class FetchingRemoteDataSniff extends AbstractFunctionParameterSniff { |
19 | 20 |
|
20 | 21 | /** |
21 | | - * Returns an array of tokens this test wants to listen for. |
| 22 | + * The group name for this group of functions. |
22 | 23 | * |
23 | | - * @return array<int|string> |
| 24 | + * @var string |
24 | 25 | */ |
25 | | - public function register() { |
26 | | - return [ T_STRING ]; |
27 | | - } |
| 26 | + protected $group_name = 'file_get_contents'; |
| 27 | + |
| 28 | + /** |
| 29 | + * Functions this sniff is looking for. |
| 30 | + * |
| 31 | + * @var array<string, bool> Key is the function name, value irrelevant. |
| 32 | + */ |
| 33 | + protected $target_functions = [ |
| 34 | + 'file_get_contents' => true, |
| 35 | + ]; |
28 | 36 |
|
29 | 37 | /** |
30 | | - * Process this test when one of its tokens is encountered. |
| 38 | + * Process the parameters of a matched function. |
31 | 39 | * |
32 | | - * @param int $stackPtr The position of the current token in the stack passed in $tokens. |
| 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. |
33 | 45 | * |
34 | 46 | * @return void |
35 | 47 | */ |
36 | | - public function process_token( $stackPtr ) { |
37 | | - |
38 | | - $functionName = $this->tokens[ $stackPtr ]['content']; |
39 | | - if ( $functionName !== 'file_get_contents' ) { |
| 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. |
40 | 52 | return; |
41 | 53 | } |
42 | 54 |
|
43 | | - $data = [ $this->tokens[ $stackPtr ]['content'] ]; |
| 55 | + $data = [ $matched_content ]; |
| 56 | + $param_start = $filename_param['start']; |
| 57 | + $search_end = ( $filename_param['end'] + 1 ); |
44 | 58 |
|
45 | | - $fileNameStackPtr = $this->phpcsFile->findNext( Tokens::$stringTokens, $stackPtr + 1, null, false, null, true ); |
46 | | - if ( $fileNameStackPtr === false ) { |
47 | | - $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.'; |
48 | | - $this->phpcsFile->addWarning( $message, $stackPtr, 'FileGetContentsUnknown', $data ); |
| 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; |
49 | 63 | } |
50 | 64 |
|
51 | | - $fileName = $this->tokens[ $fileNameStackPtr ]['content']; |
| 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 | + } |
52 | 76 |
|
53 | | - $isRemoteFile = ( strpos( $fileName, '://' ) !== false ); |
54 | 77 | if ( $isRemoteFile === true ) { |
55 | 78 | $message = '`%s()` is highly discouraged for remote requests, please use `wpcom_vip_file_get_contents()` or `vip_safe_wp_remote_get()` instead.'; |
56 | 79 | $this->phpcsFile->addWarning( $message, $stackPtr, 'FileGetContentsRemoteFile', $data ); |
| 80 | + return; |
57 | 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 no parameters were found. |
| 99 | + * |
| 100 | + * {@internal This method is only needed for handling PHP 8.1+ first class callables on WPCS < 3.2.0. |
| 101 | + * Once the minimum supported WPCS is 3.2.0 or higher, this method should be removed.} |
| 102 | + * |
| 103 | + * @param int $stackPtr The position of the current token in the stack. |
| 104 | + * @param string $group_name The name of the group which was matched. |
| 105 | + * @param string $matched_content The token content (function name) which was matched |
| 106 | + * in lowercase. |
| 107 | + * |
| 108 | + * @return void |
| 109 | + */ |
| 110 | + public function process_no_parameters( $stackPtr, $group_name, $matched_content ) { |
| 111 | + // Check if this is a first class callable. |
| 112 | + $next = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true ); |
| 113 | + if ( $next === false |
| 114 | + || $this->tokens[ $next ]['code'] !== T_OPEN_PARENTHESIS |
| 115 | + || isset( $this->tokens[ $next ]['parenthesis_closer'] ) === false |
| 116 | + ) { |
| 117 | + // Live coding/parse error. Ignore. |
| 118 | + return; |
| 119 | + } |
| 120 | + |
| 121 | + // First class callable. |
| 122 | + $firstNonEmpty = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $next + 1 ), null, true ); |
| 123 | + if ( $this->tokens[ $firstNonEmpty ]['code'] === T_ELLIPSIS ) { |
| 124 | + $secondNonEmpty = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $firstNonEmpty + 1 ), null, true ); |
| 125 | + if ( $this->tokens[ $secondNonEmpty ]['code'] === T_CLOSE_PARENTHESIS ) { |
| 126 | + $this->add_contents_unknown_warning( $stackPtr, [ $matched_content ] ); |
| 127 | + } |
| 128 | + } |
| 129 | + } |
| 130 | + |
| 131 | + /** |
| 132 | + * Process the function if it is used as a first class callable. |
| 133 | + * |
| 134 | + * @param int $stackPtr The position of the current token in the stack. |
| 135 | + * @param string $group_name The name of the group which was matched. |
| 136 | + * @param string $matched_content The token content (function name) which was matched |
| 137 | + * in lowercase. |
| 138 | + * |
| 139 | + * @return void |
| 140 | + */ |
| 141 | + public function process_first_class_callable( $stackPtr, $group_name, $matched_content ) { |
| 142 | + $this->add_contents_unknown_warning( $stackPtr, [ $matched_content ] ); |
| 143 | + } |
| 144 | + |
| 145 | + /** |
| 146 | + * Add a warning if the function is used with unknown parameter(s) or with a $filename parameter for which |
| 147 | + * it could not be determined if it references a local file or a remote file. |
| 148 | + * |
| 149 | + * @param int $stackPtr The position of the current token in the stack. |
| 150 | + * @param array<string> $data Data to use for string replacement in the error message. |
| 151 | + * |
| 152 | + * @return void |
| 153 | + */ |
| 154 | + private function add_contents_unknown_warning( $stackPtr, $data ) { |
| 155 | + $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.'; |
| 156 | + $this->phpcsFile->addWarning( $message, $stackPtr, 'FileGetContentsUnknown', $data ); |
58 | 157 | } |
59 | 158 | } |
0 commit comments