Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 75
0.00% covered (danger)
0.00%
0 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
ForbiddenFunctionsSniff
0.00% covered (danger)
0.00%
0 / 75
0.00% covered (danger)
0.00%
0 / 3
600
0.00% covered (danger)
0.00%
0 / 1
 register
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 process
0.00% covered (danger)
0.00%
0 / 44
0.00% covered (danger)
0.00%
0 / 1
182
 argCount
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
110
1<?php
2/**
3 * Copyright (C) 2017 Kunal Mehta <legoktm@member.fsf.org>
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 */
19
20namespace MediaWiki\Sniffs\Usage;
21
22use PHP_CodeSniffer\Files\File;
23use PHP_CodeSniffer\Sniffs\Sniff;
24use PHP_CodeSniffer\Util\Tokens;
25
26/**
27 * Use e.g. <exclude name="MediaWiki.Usage.ForbiddenFunctions.eval" /> in your .phpcs.xml to remove
28 * a function from the predefined list of forbidden functions.
29 *
30 * You can also add entries or modify existing ones. Note that an empty `value=""` won't work. Use
31 * "null" for forbidden functions and any other non-empty value for replacements.
32 *
33 * <rule ref="MediaWiki.Usage.ForbiddenFunctions">
34 *     <properties>
35 *         <property name="forbiddenFunctions" type="array">
36 *             <element key="eval" value="null" />
37 *             <element key="sizeof" value="count" />
38 *         </property>
39 *     </properties>
40 * </rule>
41 */
42class ForbiddenFunctionsSniff implements Sniff {
43
44    /**
45     * Predefined list of deprecated functions and their replacements, or any empty value for
46     * forbidden functions.
47     */
48    private const FORBIDDEN_FUNCTIONS = [
49        'chop' => 'rtrim',
50        'diskfreespace' => 'disk_free_space',
51        'doubleval' => 'floatval',
52        'ini_alter' => 'ini_set',
53        'is_integer' => 'is_int',
54        'is_long' => 'is_int',
55        'is_double' => 'is_float',
56        'is_real' => 'is_float',
57        'is_writeable' => 'is_writable',
58        'join' => 'implode',
59        'key_exists' => 'array_key_exists',
60        'pos' => 'current',
61        'sizeof' => 'count',
62        'strchr' => 'strstr',
63        'assert' => false,
64        'eval' => false,
65        'extract' => false,
66        'compact' => false,
67        // Deprecated in PHP 7.2
68        'create_function' => false,
69        'each' => false,
70        'parse_str' => false,
71        'mb_parse_str' => false,
72        // MediaWiki wrappers for external program execution should be used,
73        // forbid PHP's (https://secure.php.net/manual/en/ref.exec.php)
74        'escapeshellarg' => false,
75        'escapeshellcmd' => false,
76        'exec' => false,
77        'passthru' => false,
78        'popen' => false,
79        'proc_open' => false,
80        'shell_exec' => false,
81        'system' => false,
82        'isset' => false,
83        // resource type is going away in PHP 8.0+ (T260735)
84        'is_resource' => false,
85        // define third parameter is deprecated in 7.3
86        'define' => false,
87    ];
88
89    /**
90     * Functions that are forbidden (per above) but allowed with a specific number of arguments
91     */
92    private const ALLOWED_ARG_COUNT = [
93        'parse_str' => 2,
94        'mb_parse_str' => 2,
95        'isset' => 1,
96        'define' => 2,
97    ];
98
99    /**
100     * @var string[] Key-value pairs as provided via .phpcs.xml. Maps deprecated function names to
101     *  their replacement, or the literal string "null" for forbidden functions.
102     */
103    public $forbiddenFunctions = [];
104
105    /**
106     * @inheritDoc
107     */
108    public function register(): array {
109        return [ T_STRING, T_EVAL, T_ISSET ];
110    }
111
112    /**
113     * @param File $phpcsFile
114     * @param int $stackPtr The current token index.
115     * @return void
116     */
117    public function process( File $phpcsFile, $stackPtr ) {
118        $tokens = $phpcsFile->getTokens();
119
120        $nextToken = $phpcsFile->findNext( T_WHITESPACE, $stackPtr + 1, null, true );
121        if ( $tokens[$nextToken]['code'] !== T_OPEN_PARENTHESIS ||
122            !isset( $tokens[$nextToken]['parenthesis_closer'] )
123        ) {
124            return;
125        }
126
127        // Check if the function is one of the bad ones
128        $funcName = $tokens[$stackPtr]['content'];
129        if ( array_key_exists( $funcName, $this->forbiddenFunctions ) ) {
130            $replacement = $this->forbiddenFunctions[$funcName];
131            if ( $replacement === $funcName ) {
132                return;
133            }
134        } elseif ( array_key_exists( $funcName, self::FORBIDDEN_FUNCTIONS ) ) {
135            $replacement = self::FORBIDDEN_FUNCTIONS[$funcName];
136        } else {
137            return;
138        }
139
140        $ignore = [
141            T_DOUBLE_COLON => true,
142            T_OBJECT_OPERATOR => true,
143            T_NULLSAFE_OBJECT_OPERATOR => true,
144            T_FUNCTION => true,
145            T_CONST => true,
146        ];
147
148        // Check to make sure it's a PHP function (not $this->, etc.)
149        $prevToken = $phpcsFile->findPrevious( T_WHITESPACE, $stackPtr - 1, null, true );
150        if ( isset( $ignore[$tokens[$prevToken]['code']] ) ) {
151            return;
152        }
153
154        // Check argument count
155        $allowedArgCount = self::ALLOWED_ARG_COUNT[$funcName] ?? null;
156        if ( $allowedArgCount !== null &&
157            $this->argCount( $phpcsFile, $nextToken ) == $allowedArgCount
158        ) {
159            // Nothing to replace
160            return;
161        }
162
163        // The hard-coded FORBIDDEN_FUNCTIONS can use false, but values from .phpcs.xml are always
164        // strings. We use the same special string "null" as in the Generic.PHP.ForbiddenFunctions
165        // sniff.
166        if ( $replacement && $replacement !== 'null' ) {
167            $fix = $phpcsFile->addFixableWarning(
168                'Use %s() instead of %s',
169                $stackPtr,
170                $funcName,
171                [ $replacement, $funcName ]
172            );
173            if ( $fix ) {
174                $phpcsFile->fixer->replaceToken( $stackPtr, $replacement );
175            }
176        } else {
177            $phpcsFile->addWarning(
178                $allowedArgCount !== null
179                    ? '%s should be used with %s argument(s)'
180                    : '%s should not be used',
181                $stackPtr,
182                $funcName,
183                [ $funcName, $allowedArgCount ]
184            );
185        }
186    }
187
188    /**
189     * Return the number of arguments between the $parenthesis as opener and its closer
190     * Ignoring commas between brackets to support nested argument lists
191     *
192     * @param File $phpcsFile
193     * @param int $parenthesis The parenthesis token index.
194     * @return int
195     */
196    private function argCount( File $phpcsFile, int $parenthesis ): int {
197        $tokens = $phpcsFile->getTokens();
198        $end = $tokens[$parenthesis]['parenthesis_closer'];
199        $next = $phpcsFile->findNext( Tokens::$emptyTokens, $parenthesis + 1, $end, true );
200        $argCount = 0;
201
202        if ( $next !== false ) {
203            // Something found, there is at least one argument
204            $argCount++;
205
206            $searchTokens = [
207                T_OPEN_CURLY_BRACKET,
208                T_OPEN_SQUARE_BRACKET,
209                T_OPEN_SHORT_ARRAY,
210                T_OPEN_PARENTHESIS,
211                T_COMMA
212            ];
213            while ( $next !== false ) {
214                switch ( $tokens[$next]['code'] ) {
215                    case T_OPEN_CURLY_BRACKET:
216                    case T_OPEN_SQUARE_BRACKET:
217                    case T_OPEN_PARENTHESIS:
218                        if ( isset( $tokens[$next]['parenthesis_closer'] ) ) {
219                            // jump to closing parenthesis to ignore commas between opener and closer
220                            $next = $tokens[$next]['parenthesis_closer'];
221                        }
222                        break;
223                    case T_OPEN_SHORT_ARRAY:
224                        if ( isset( $tokens[$next]['bracket_closer'] ) ) {
225                            // jump to closing bracket to ignore commas between opener and closer
226                            $next = $tokens[$next]['bracket_closer'];
227                        }
228                        break;
229                    case T_COMMA:
230                        $argCount++;
231                        break;
232                }
233
234                $next = $phpcsFile->findNext( $searchTokens, $next + 1, $end );
235            }
236        }
237
238        return $argCount;
239    }
240
241}