Completed
Push — master ( 8851bc...2533f0 )
by Jeremy
32:27 queued 21:03
created

vp-scanner.php ➔ vp_scan_file()   F

Complexity

Conditions 35
Paths > 20000

Size

Total Lines 126

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 35
nc 31126
nop 3
dl 0
loc 126
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
// don't call the file directly
3
defined( 'ABSPATH' ) or die();
4
5
class VP_FileScan {
6
	var $path;
7
	var $last_dir = null;
8
	var $offset = 0;
9
	var $ignore_symlinks = false;
10
11
	function __construct( $path, $ignore_symlinks = false ) {
12
		if ( is_dir( $path ) )
13
			$this->last_dir = $this->path = @realpath( $path );
14
		else
15
			$this->last_dir = $this->path = dirname( @realpath( $path ) );
16
		$this->ignore_symlinks = $ignore_symlinks;
17
	}
18
19
	function get_files( $limit = 100 ) {
20
		$files = array();
21
		if ( is_dir( $this->last_dir ) ) {
22
			$return = $this->_scan_files( $this->path, $files, $this->offset, $limit, $this->last_dir );
23
			$this->offset = $return[0];
24
			$this->last_dir = $return[1];
25
			if ( count( $files ) < $limit )
26
				$this->last_dir = false;
27
		}
28
		return $files;
29
	}
30
31
	function _scan_files( $path, &$files, $offset, $limit, &$last_dir ) {
32
		$_offset = 0;
33
		if ( is_readable( $path ) && $handle = opendir( $path ) ) {
34
			while( false !== ( $entry = readdir( $handle ) ) ) {
35
				if ( '.' == $entry || '..' == $entry )
36
					continue;
37
38
				$_offset++;
39
				$full_entry = $path . DIRECTORY_SEPARATOR . $entry;
40
				$next_item = ltrim( str_replace( $path, '', $last_dir ), DIRECTORY_SEPARATOR );
41
				$next = preg_split( '#(?<!\\\\)' . preg_quote( DIRECTORY_SEPARATOR, '#' ) . '#', $next_item, 2 );
42
43
				// Skip if the next item is not found.
44
				if ( !empty( $next[0] ) && $next[0] != $entry )
45
					continue;
46
				if ( rtrim( $last_dir, DIRECTORY_SEPARATOR ) == rtrim( $path, DIRECTORY_SEPARATOR ) && $_offset < $offset )
47
					continue;
48
				if ( $this->ignore_symlinks && is_link( $full_entry ) )
49
					continue;
50
51
				if ( rtrim( $last_dir, DIRECTORY_SEPARATOR ) == rtrim( $path, DIRECTORY_SEPARATOR ) ) {
52
					// Reset last_dir and offset when we reached the previous last_dir value.
53
					$last_dir = '';
54
					$offset = 0;
55
				}
56
57
				if ( is_file( $full_entry ) ) {
58
					if ( !vp_is_interesting_file( $full_entry ) )
59
						continue;
60
					$_return_offset = $_offset;
61
					$_return_dir = dirname( $full_entry );
62
					$files[] = $full_entry;
63
				} elseif ( is_dir( $full_entry ) ) {
64
					list( $_return_offset, $_return_dir ) = $this->_scan_files( $full_entry, $files, $offset, $limit, $last_dir );
65
				}
66
				if ( count( $files ) >= $limit ) {
67
					closedir( $handle );
68
					return array( $_return_offset, $_return_dir );
0 ignored issues
show
Bug introduced by
The variable $_return_dir does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
Bug introduced by
The variable $_return_offset does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
69
				}
70
			}
71
			closedir( $handle );
72
		}
73
		return array( $_offset, $path );
74
	}
75
}
76
77
function vp_get_real_file_path( $file_path, $tmp_file = false ) {
78
	global $site, $site_id;
79
	$site_id = !empty( $site->id ) ? $site->id : $site_id;
80
	if ( !$tmp_file && !empty( $site_id ) && function_exists( 'determine_file_type_path' ) ) {
81
		$path = determine_file_type_path( $file_path );
82
		$file = file_by_path( $site_id, $path );
83
		if ( !$file )
84
			return false;
85
		return $file->get_unencrypted();
86
	}
87
	return !empty( $tmp_file ) ? $tmp_file : $file_path;
88
}
89
90
function vp_is_interesting_file($file) {
91
	$scan_only_regex = apply_filters( 'scan_only_extension_regex', '#\.(ph(p3|p4|p5|p|tml)|html|js|htaccess)$#i' );
92
	return preg_match( $scan_only_regex, $file );
93
}
94
95
/**
96
 * Uses the PHP tokenizer to split a file into 3 arrays: PHP code with no comments,
97
 * PHP code with comments, and HTML/JS code. Helper wrapper around split_to_php_html()
98
 *
99
 * @param string $file The file path to read and parse
100
 * @return array An array with 3 arrays of lines
101
 */
102
function split_file_to_php_html( $file ) {
103
	$source = @file_get_contents( $file );
104
	if ( $source === false ) {
105
		$source = '';
106
	}
107
	return split_to_php_html( $source );
108
}
109
110
/**
111
 * Uses the PHP tokenizer to split a string into 3 arrays: PHP code with no comments,
112
 * PHP code with comments, and HTML/JS code.
113
 *
114
 * @param string $file The file path to read and parse
0 ignored issues
show
Bug introduced by
There is no parameter named $file. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
115
 * @return array An array with 3 arrays of lines
116
 */
117
function split_to_php_html( $source ) {
118
	$tokens = @token_get_all( $source );
119
120
	$ret = array( 'php' => array(), 'php-with-comments' => array(), 'html' => array() );
121
	$current_line = 0;
122
	$mode = 'html'; // need to see an open tag to switch to PHP mode
123
124
	foreach ( $tokens as $token ) {
125
		if ( ! is_array( $token ) ) {
126
			// single character, can't switch our mode; just add it and continue
127
			// if it's PHP, should go into both versions; mode 'php' will do that
128
			add_text_to_parsed( $ret, $mode, $current_line, $token );
129
			$current_line += substr_count( $token, "\n" );
130
		} else {
131
			// more complex tokens is the interesting case
132
			list( $id, $text, $line ) = $token;
0 ignored issues
show
Unused Code introduced by
The assignment to $line is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
133
			
134
			if ( 'php' === $mode ) {
135
				// we're in PHP code
136
137
				// might be a comment
138
				if ( T_COMMENT === $id || T_DOC_COMMENT === $id ) {
139
					// add it to the PHP with comments array only
140
					add_text_to_parsed( $ret, 'php-with-comments', $current_line, $text );
141
142
					// special case for lines like: "     // comment\n":
143
					// if we're adding a comment with a newline, and the 'php' array current line
144
					// has no trailing newline, add one
145
					if ( substr_count( $text, "\n" ) >= 1 && isset( $ret['php'][ $current_line ] ) && 0 === substr_count( $ret['php'][ $current_line ], "\n" ) ) {
146
						$ret['php'][ $current_line ] .= "\n";
147
					}
148
149
					// make sure to count newlines in comments 
150
					$current_line += substr_count( $text, "\n" );
151
					continue;
152
				}
153
154
				// otherwise add it to both the PHP array and the with comments array
155
				add_text_to_parsed( $ret, $mode, $current_line, $text );
156
157
				// then see if we're breaking out
158
				if ( T_CLOSE_TAG === $id ) {
159
					$mode = 'html';
160
				}
161
			} else if ( 'html' === $mode ) {
162
				// we're in HTML code
163
164
				// if we see an open tag, switch to PHP
165
				if ( T_OPEN_TAG === $id || T_OPEN_TAG_WITH_ECHO === $id ) {
166
					$mode = 'php';
167
				}
168
169
				// add to the HTML array (or PHP if it was an open tag)
170
				// if it is PHP, this will add it to both arrays, which is what we want
171
				add_text_to_parsed( $ret, $mode, $current_line, $text );
172
			}
173
			$current_line += substr_count( $text, "\n" );
174
		}
175
	}
176
177
	return $ret;
178
}
179
180
/**
181
 * Helper function for split_file_to_php_html; adds a chunk of text to the arrays we'll return.
182
 * @param array $parsed The array containing all the languages we'll return
183
 * @param string $prefix The prefix for the languages we want to add this text to
184
 * @param int $line_number The line number that this text goes on
0 ignored issues
show
Documentation introduced by
There is no parameter named $line_number. Did you maybe mean $start_line_number?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
185
 * @param string $text The text to add
0 ignored issues
show
Bug introduced by
There is no parameter named $text. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
186
 */
187
function add_text_to_parsed( &$parsed, $prefix, $start_line_number, $all_text ) {
188
	$line_number = $start_line_number;
189
190
	// whitespace tokens may span multiple lines; we need to split them up so that the indentation goes on the next line
191
	$fragments = explode( "\n", $all_text );
192
	foreach ( $fragments as $i => $fragment ) {
193
		// each line needs to end with a newline to match the behavior of file()
194
		if ( $i < count( $fragments ) - 1 ) {
195
			$text = $fragment . "\n";
196
		} else {
197
			$text = $fragment;
198
		}
199
200
		if ( '' === $text ) {
201
			// check for the empty string explicitly, rather than using empty()
202
			// otherwise things like a '0' token will get skipped, because PHP is stupid
203
			continue;
204
		}
205
206
		if ( ! isset( $parsed[ $prefix ][ $line_number ] ) ) {
207
			$parsed[ $prefix ][ $line_number ] = '';
208
		}
209
		$parsed[ $prefix ][ $line_number ] .= $text;
210
		if ( 'php' == $prefix ) {
211
			if ( ! isset( $parsed[ 'php-with-comments' ][ $line_number ] ) ) {
212
				$parsed[ 'php-with-comments' ][ $line_number ] = '';
213
			}
214
			$parsed[ 'php-with-comments' ][ $line_number ] .= $text;
215
		}
216
217
		// the caller will also update their line number based on the number of \n characters in the text
218
		$line_number++;
219
	}
220
}
221
/**
222
 * Scans a file with the registered signatures. To report a security notice for a specified signature, all its regular
223
 * expressions should result in a match.
224
 * @param $file the filename to be scanned.
225
 * @param null $tmp_file used if the file to be scanned doesn't exist or if the filename doesn't match vp_is_interesting_file().
226
 * @return array|bool false if no matched signature is found. A list of matched signatures otherwise.
227
 */
228
function vp_scan_file( $file, $tmp_file = null, $use_parser = false ) {
229
	$real_file = vp_get_real_file_path( $file, $tmp_file );
0 ignored issues
show
Documentation introduced by
$tmp_file is of type null, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
230
	$file_size = file_exists( $real_file ) ? @filesize( $real_file ) : 0;
231
	if ( !is_readable( $real_file ) || !$file_size || $file_size > apply_filters( 'scan_max_file_size', 3 * 1024 * 1024 ) ) { // don't scan empty or files larger than 3MB.
232
		return false;
233
	}
234
235
	$file_content = null;
236
	$file_parsed = null;
237
	$skip_file = apply_filters_ref_array( 'pre_scan_file', array ( false, $file, $real_file, &$file_content ) );
238
	if ( false !== $skip_file ) { // maybe detect malware without regular expressions.
239
		return $skip_file;
240
	}
241
242
	if ( !vp_is_interesting_file( $file ) ) { // only scan relevant files.
243
		return false;
244
	}
245
246
	if ( !isset( $GLOBALS['vp_signatures'] ) ) {
247
		$GLOBALS['vp_signatures'] = array();
248
	}
249
250
	$found = array ();
251
	foreach ( $GLOBALS['vp_signatures'] as $signature ) {
252
		if ( !is_object( $signature ) || !isset( $signature->patterns ) ) {
253
			continue;
254
		}
255
		// if there is no filename_regex, we assume it's the same of vp_is_interesting_file().
256
		if ( empty( $signature->filename_regex ) || preg_match( '#' . addcslashes( $signature->filename_regex, '#' ) . '#i', $file ) ) {
257
			if ( null === $file_content || !is_array( $file_content ) ) {
258
				$file_content = @file( $real_file );
259
260
				if ( $file_content === false ) {
261
					return false;
262
				}
263
264
				if ( $use_parser ) {
265
					$file_parsed = split_file_to_php_html( $real_file );
266
				}
267
			}
268
269
			$is_vulnerable = true;
270
271
			$code = $file_content;
272
273
			if ( $use_parser ) {
274
				// use the language specified in the signature if it has one
275
				if ( ! empty( $signature->target_language ) && array_key_exists( $signature->target_language, $file_parsed ) ) {
276
					$code = $file_parsed[ $signature->target_language ];
277
278
279
				}
280
			}
281
282
			$matches = array();
283
			if ( ! empty( $signature->patterns ) ) {
284
				foreach ( $signature->patterns as $pattern ) {
285
					$match = preg_grep( '#' . addcslashes( $pattern, '#' ) . '#im', $code );
286
					if ( empty( $match ) ) {
287
						$is_vulnerable = false;
288
						break;
289
					}
290
291
					$matches += $match;
292
				}
293
			}
294
295
			// convert the matched line to an array of details showing context around the lines
296
			$lines = array();
297
298
			$lines_parsed = array();
299
300
			$line_indices_parsed = array();
301
302
			if ( $use_parser ) {
303
				$line_indices_parsed = array_keys( $code );
304
			}
305
306
			foreach ( $matches as $line => $text ) {
307
				$lines = array_merge( $lines, range( $line - 1, $line + 1 ) );
308
				if ( $use_parser ) {
309
					$idx = array_search( $line, $line_indices_parsed );
310
311
					// we might be looking at the first or last line; for the non-parsed case, array_intersect_key
312
					// handles this transparently below; for the parsed case, since we have another layer of
313
					// indirection, we have to handle that case here
314
					$idx_around = array();
315
					if ( isset( $line_indices_parsed[ $idx - 1 ] ) ) {
316
						$idx_around[] = $line_indices_parsed[ $idx - 1 ];
317
					}
318
					$idx_around[] = $line_indices_parsed[ $idx ];
319
					if ( isset( $line_indices_parsed[ $idx + 1 ] ) ) {
320
						$idx_around[] = $line_indices_parsed[ $idx + 1 ];
321
					}
322
					$lines_parsed = array_merge( $lines_parsed, $idx_around );
323
				}
324
			}
325
326
			$details = array_intersect_key( $file_content, array_flip( $lines ) );
327
328
			$details_parsed = array();
329
330
			if ( $use_parser ) {
331
				$details_parsed = array_intersect_key( $code, array_flip( $lines_parsed ) );
332
			}
333
334
			// provide both 'matches' and 'details', as some places want 'matches'
335
			// this matches the old behavior, which would add 'details' to some items, without replacing 'matches'
336
			$debug_data = array( 'matches' => $matches, 'details' => $details  );
337
			if ( $use_parser ) {
338
				$debug_data['details_parsed'] = $details_parsed;
339
			}
340
341
			// Additional checking needed?
342
			if ( method_exists( $signature, 'get_detailed_scanner' ) && $scanner = $signature->get_detailed_scanner() )
343
				$is_vulnerable = $scanner->scan( $is_vulnerable, $file, $real_file, $file_content, $debug_data );
344
			if ( $is_vulnerable ) {
345
				$found[$signature->id] = $debug_data;
346
				if ( isset( $signature->severity ) && $signature->severity > 8 ) // don't continue scanning
347
					break;
348
			}
349
		}
350
	}
351
352
	return apply_filters_ref_array( 'post_scan_file', array ( $found, $file, $real_file, &$file_content ) );
353
}
354