HEX
Server: Apache
System: Linux WWW 6.1.0-40-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.153-1 (2025-09-20) x86_64
User: web11 (1011)
PHP: 8.2.29
Disabled: NONE
Upload Files
File: /var/www/biblioteka/wp-content/plugins/ninjascanner/lib/scan.php
<?php
/*
 +=====================================================================+
 |     _   _ _        _       ____                                     |
 |    | \ | (_)_ __  (_) __ _/ ___|  ___ __ _ _ __  _ __   ___ _ __    |
 |    |  \| | | '_ \ | |/ _` \___ \ / __/ _` | '_ \| '_ \ / _ \ '__|   |
 |    | |\  | | | | || | (_| |___) | (_| (_| | | | | | | |  __/ |      |
 |    |_| \_|_|_| |_|/ |\__,_|____/ \___\__,_|_| |_|_| |_|\___|_|      |
 |                 |__/                                                |
 |                                                                     |
 | (c) NinTechNet ~ https://nintechnet.com/                            |
 +=====================================================================+
*/
if (! defined( 'ABSPATH' ) ) { die( 'Forbidden' ); }

// =====================================================================
// Get scan's status
$lock_status = nscan_get_lock_status();
if ( empty( $lock_status['current_step'] ) ) {
	nscan_log_error( sprintf(
		__('Missing %s value. Exiting scanning process', 'ninjascanner'),
		"'current_step'"
	) );
	touch( NSCAN_CANCEL );
}
if ( $lock_status['status'] != 'success' ) {
	nscan_log_error( sprintf(
		__('Missing %s value. Exiting scanning process', 'ninjascanner'),
		"'status'"
	) );
	exit;
}

global $nscan_steps;
global $snapshot; $snapshot = array();
global $current_snapshot; $current_snapshot = array();

// Ignored files list
global $ignored_files;
$ignored_files = array();
if ( file_exists( NSCAN_IGNORED_LOG ) ) {
	$ignored_files = unserialize( file_get_contents( NSCAN_IGNORED_LOG ) );
}

if ( file_exists( NSCAN_TMP_SNAPSHOT ) ) {
	$snapshot = unserialize( file_get_contents( NSCAN_TMP_SNAPSHOT ) );
	if ( empty( $snapshot['sys']['starttime'] ) ) {
		$msg = __('Fatal error: Snapshot seems to be corrupted.', 'ninjascanner');
		nscan_log_error( $msg );
		touch( NSCAN_CANCEL );
		nscan_set_lock_status(
			$lock_status['current_step'],
			'cancelled',
			$msg
		);
		exit;
	}
}

@set_time_limit( 0 );
@ini_set('memory_limit', -1 );

register_shutdown_function('nscan_erroronexit');

if ( version_compare( PHP_VERSION, '7.3', '<' ) ) {
	$snapshot['sys']['metrics'] = 'microtime';
} else {
	$snapshot['sys']['metrics'] = 'hrtime';
}
$snapshot['sys']['starttime'] = $snapshot['sys']['metrics']( true );

nscan_log_info( sprintf(
	__('Processing step %s/%s', 'ninjascanner'),
	$lock_status['current_step'], count( $nscan_steps )
) );
$snapshot['sys']['startmem'] = memory_get_peak_usage( false );
$nscan_steps[ $lock_status['current_step'] ]( $lock_status );
nscan_memory_used( $snapshot['sys']['starttime'], $snapshot['sys']['startmem'], $snapshot['sys']['metrics'] );

$lock_status = nscan_get_lock_status();
if (! empty( $nscan_steps[ $lock_status['current_step'] ] ) ) {
	// Fork another process
	$_POST['nscan_key'] = nscan_generate_key();
	$return = array();
	$return = json_decode( nscan_ajax_startscan(), true );
	if ( $return['status'] != 'success' ) {
		$msg = sprintf(
			__('Fatal error: process returned %s (%s).', 'ninjascanner'),
			$return['status'],
			$return['message']
		);
		nscan_log_error( $msg );
		touch( NSCAN_CANCEL );
		nscan_set_lock_status(
			$lock_status['current_step'],
			'cancelled',
			$msg
		);
		exit;
	}

} else {
	// Save and quit
	if ( $lock_status['current_step'] > count( $nscan_steps ) ) {
		if ( file_exists( NSCAN_TMP_SNAPSHOT ) ) {
			// Save old one
			if ( file_exists( NSCAN_SNAPSHOT ) ) {
				rename( NSCAN_SNAPSHOT, NSCAN_OLD_SNAPSHOT );
			}
			// Save new one
			rename( NSCAN_TMP_SNAPSHOT, NSCAN_SNAPSHOT );
		}
	}
	nscan_set_lock_status(
		--$lock_status['current_step'], // Decrement it
		'stopped',
		''
	);
	// Send report
	nscan_send_email();
	nscan_log_info( __('Exiting scanning process', 'ninjascanner') );
	delete_transient( 'nscan_temp_sigs' );
}

// =====================================================================
// Send a email report.

function nscan_send_email() {

	global $snapshot;
	// Shall we send the report by email?
	$nscan_options = get_option( 'nscan_options' );
	if (! empty( $nscan_options['admin_email'] ) ) {
		require __DIR__ .'/report_email.php';
		nscan_send_email_report( $snapshot, $nscan_options );
	}
}

// =====================================================================
// Catch potential errors.

function nscan_erroronexit() {

	global $lock_status;

	$e = error_get_last();
	if ( isset( $e['type'] ) && $e['type'] === E_ERROR ) {
		$err = str_replace( "\n", ' - ', $e['message'] );
		nscan_log_error( sprintf(
			__('Error: E_ERROR (%s - line %s in %s)', 'ninjascanner'),
			$err,
			$e['line'],
			$e['file']
		));

		nscan_set_lock_status(
			$lock_status['current_step'],
			'error',
			$e['message']
		);
		$nscan_options = get_option( 'nscan_options' );
		if (! empty( $nscan_options['admin_email'] ) ) {
			$recipient = $nscan_options['admin_email'];
		} else {
			$recipient = get_option('admin_email');
		}
		$subject = '[NinjaScanner] ' . __('Scan error', 'ninjascanner');
		$message = __('An fatal error occurred during the scan:', 'ninjascanner') ."\n";
		$message .= $e['message'] ."\n\n";
		$message.= __('Blog:', 'ninjascanner') .' '. home_url('/') . "\n";
		$message.= __('Date:', 'ninjafirewall') .' '. date('F j, Y @ H:i:s') . ' (UTC '. date('O') . ")\n\n";
		wp_mail( $recipient, $subject, $message );

		global $snapshot;
		nscan_memory_used( $snapshot['sys']['starttime'], $snapshot['sys']['startmem'], $snapshot['sys']['metrics'] );
	}
}

// =====================================================================
// Check NinjaScanner's files integrity by downloading its checksum
// hashes - or using the local cached copy. In case of mismatch,
// refuse to run (users can still bypass this by disabling the integrity
// checker from the settings page).

function nscan_check_scanner_integrity( $lock_status ) {

	$nscan_options = get_option( 'nscan_options' );

	// Are we supposed to do that
	if (! $nscan_options['scan_ninjaintegrity'] ) {
		$message = __('Skipping NinjaScanner files integrity check', 'ninjascanner' );
		nscan_log_info( $message );
		// Next step
		nscan_set_lock_status(
			++$lock_status['current_step'],
			'success',
			$message
		);
		return;
	}

	$message = __('Checking NinjaScanner files integrity', 'ninjascanner');
	nscan_log_info( $message );
	nscan_set_lock_status(
		$lock_status['current_step'],
		'success',
		$message
	);

	if ( ( $res = nscan_check_ninjascanner_integrity() ) === true ) {
		nscan_log_info( __('Files integrity is O.K', 'ninjascanner') );
	} else {
		if ( $res === false ) {
			// Failed! We warn and quit
			touch( NSCAN_CANCEL );
			nscan_set_lock_status(
				$lock_status['current_step'],
				'cancelled',
				__('Fatal error: NinjaScanner files integrity check: Decoded hashes seem corrupted. Aborting.', 'ninjascanner')
			);
			exit;
		} else {
			// The server may be down. Clear the 'scan_ninjaintegrity' flag,
			// we'll attempt to check the plugin again while checking all
			// plugin files integrity
			$nscan_options['scan_ninjaintegrity'] = 0;
			nscan_log_debug( __("Clearing 'scan_ninjaintegrity', we'll check the plugin again later", 'ninjascanner') );
		}
	}

	nscan_set_lock_status(
		++$lock_status['current_step'],
		'success',
		$message
	);
}

// =====================================================================
// Retrieve the list of files.

function nscan_build_files_list( $lock_status ) {

	global $snapshot, $ignored_files;

	// Clear temp data
	unset( $snapshot['tmp'] );
	delete_transient( 'nscan_temp_sigs' );

	// Save WP version
	global $wp_version;
	$snapshot['version'] = $wp_version;
	// Save locale
	global $wp_local_package;
	if (! empty( $wp_local_package ) ) {
		$snapshot['locale'] = $wp_local_package;
	}
	// Save NinjaScanner's version
	$snapshot['nscan_version'] = NSCAN_VERSION;

	$nscan_options = get_option( 'nscan_options' );

	$message = __("Building file's list", 'ninjascanner' );
	nscan_log_info( $message );
	nscan_set_lock_status(
		$lock_status['current_step'],
		'success',
		$message
	);

	// Get rid of any trailing slash, unless it's a chrooted WP:
	$abspath = realpath( ABSPATH );
	// Exclude root folders
	$excluded_root_folders = array();
	if (! empty( $nscan_options['scan_root_folders'] ) ) {
		$erf = json_decode( $nscan_options['scan_root_folders'], true );
		foreach( $erf as $folder ) {
			$tmp = realpath( "$abspath/$folder" );
			if ( $tmp !== false ) {
				$excluded_root_folders[] = $tmp;
			}
		}
	}
	$arg = array(
		'abspath'					=> $abspath,
		'excluded_root_folders' => $excluded_root_folders,
		'scan_nosymlink'			=> $nscan_options['scan_nosymlink'],
		'scan_warnsymlink'		=> $nscan_options['scan_warnsymlink'],
		'scan_warnhiddenphp'		=> $nscan_options['scan_warnhiddenphp'],
		'scan_warnunreadable'	=> $nscan_options['scan_warnunreadable'],
		'scan_warnbinary'			=> $nscan_options['scan_warnbinary'],
		'plugin_dir'				=> realpath( WP_PLUGIN_DIR ),
		'theme_dir'					=> realpath( WP_CONTENT_DIR .'/themes' )
	);
	_nscan_build_files_list( $arg['abspath'], $arg );

	// Save ignored list as it may have changed
	file_put_contents( NSCAN_IGNORED_LOG, serialize( $ignored_files ) );

	// Make sure we have some files:
	if ( empty( $snapshot['abspath'] ) ) {
		$msg = __('Fatal error: No file found. Check your NinjaScanner configuration. Aborting.', 'ninjascanner');
		$snapshot['error'] = $msg;
		nscan_log_error( $msg );
		touch( NSCAN_CANCEL );
		nscan_set_lock_status(
			$lock_status['current_step'],
			'cancelled',
			$msg
		);
		exit;
	}

	nscan_log_info( sprintf(
		__('Total files found: %s', 'ninjascanner'),
		number_format_i18n( count( $snapshot['abspath'] ) )
	));

	if (! empty( $snapshot['core_symlink'] ) ) {
		nscan_log_warn( sprintf(
			__('Symlinks found: %s', 'ninjascanner'),
			number_format_i18n( count( $snapshot['core_symlink'] ) )
		));
	}
	if ( empty( $nscan_options['scan_warnsymlink'] ) ) {
		$snapshot['skip']['core_symlink'] = 1;
	}

	if (! empty( $snapshot['core_unreadable'] ) ) {
		nscan_log_warn( sprintf(
			__('Unreadable files found: %s', 'ninjascanner'),
			number_format_i18n( count( $snapshot['core_unreadable'] ) )
		));
	}
	if ( empty( $nscan_options['scan_warnunreadable'] ) ){
		$snapshot['skip']['core_unreadable'] = 1;
	}

	if (! empty( $snapshot['core_hidden'] ) ) {
		nscan_log_warn( sprintf(
			__('Hidden scripts found: %s', 'ninjascanner'),
			number_format_i18n( count( $snapshot['core_hidden'] ) )
		));
	}
	if ( empty( $nscan_options['scan_warnhiddenphp'] ) ) {
		$snapshot['skip']['core_hidden'] = 1;
	}

	// Create the database posts and pages checksum:
	global $wpdb;
	$snapshot['posts'] = array(); $snapshot['pages'] = array();
	nscan_log_info( __('Building database posts and pages checksum', 'ninjascanner') );
	// Posts:
	$tmp_array = $wpdb->get_results(
		"SELECT ID, post_title, sha1(concat(post_content, post_title, post_excerpt, post_name))
		as hash
		FROM {$wpdb->posts}
		WHERE `post_type` = 'post' and `post_status` = 'publish'"
	);
	foreach( $tmp_array as $item ) {
		$snapshot['posts'][$item->ID]['permalink'] = get_permalink( $item->ID );
		$snapshot['posts'][$item->ID]['hash'] = $item->hash;
	}
	unset($tmp_array);
	// Pages:
	$tmp_array = $wpdb->get_results(
		"SELECT ID, post_title, sha1(concat(post_content, post_title, post_excerpt, post_name))
		as hash
		FROM {$wpdb->posts}
		WHERE `post_type` = 'page' and `post_status` = 'publish'"
	);
	foreach( $tmp_array as $item ) {
		$snapshot['pages'][$item->ID]['permalink'] = get_permalink( $item->ID );
		$snapshot['pages'][$item->ID]['hash'] = $item->hash;
	}
	unset($tmp_array);
	nscan_log_info( sprintf(
		__('Found %s posts and %s pages in the database', 'ninjascanner'),
		count( $snapshot['posts'] ),
		count( $snapshot['pages'] )
	) );

	// Save snapshot
	file_put_contents( NSCAN_TMP_SNAPSHOT, serialize( $snapshot ) );

	nscan_set_lock_status(
		++$lock_status['current_step'],
		'success',
		$message
	);
}

// ---------------------------------------------------------------------

function _nscan_build_files_list( $scan_dir, $arg ) {

	global $snapshot, $ignored_files;

  if ( is_dir( $scan_dir ) && is_readable( $scan_dir ) ) {

		// User-excluded root folders
		if ( in_array( $scan_dir, $arg['excluded_root_folders'] ) ) {
			return;
		}

		if ( $dh = opendir( $scan_dir ) ) {
			while ( FALSE !== ( $file = readdir($dh) ) ) {

				if ( $file == '.' || $file == '..' ) { continue; }
				if ( strpos( $scan_dir, NSCAN_QUARANTINE ) !== false ) {
					continue;
				}
				$full_path = $scan_dir . '/' . $file;

				// Check if it is in the ignored files list:
				if (! empty( $ignored_files[$full_path] ) ) {
					if ( $ignored_files[$full_path] == filectime( $full_path ) ) {
						continue;

					} else {
						unset( $ignored_files[$full_path] );
					}
				}

				if ( is_readable( $full_path ) ) {
					// Directory:
					if ( is_dir( $full_path ) ) {
						if ( is_link( $full_path ) ) {
							if ( $arg['scan_warnsymlink'] ) {
								$snapshot['core_symlink'][$full_path] = 1;
							}
							// Follow symlinks?
							if ( $arg['scan_nosymlink'] ) { continue; }
						}
						_nscan_build_files_list( $full_path, $arg );

					// File:
					} elseif ( is_file( $full_path ) ) {
						if ( $arg['scan_warnsymlink'] && is_link( $full_path ) ) {
							$snapshot['core_symlink'][$full_path] = 1;
						}

						$snapshot['abspath'][$full_path][0] = filectime( $full_path );
						$snapshot['abspath'][$full_path][1] = filesize( $full_path );
						if ( strpos( $full_path, "{$arg['plugin_dir']}/" ) !== false ) {
							$str = substr( $full_path, strlen( $arg['plugin_dir'] ) + 1 );
							$list = explode( '/', $str, 2 );
							// Don't add plugins/hello.php and plugins/index.php, we'll check them with core files:
							if ( $list[0] != 'hello.php' &&  $list[0] != 'index.php' && isset( $list[1] ) ) {
								$snapshot['plugins'][$list[0]][$list[1]] = 0;
							}
							continue;
						}
						if ( strpos( $full_path, "{$arg['theme_dir']}/" ) !== false ) {
							$str = substr( $full_path, strlen( $arg['theme_dir'] ) + 1 );
							$list = explode( '/', $str, 2 );
							// Don't add themes/index.php, we'll check it with core files:
							if ( $list[0] != "index.php" && isset( $list[1] ) ) {
								$snapshot['themes'][$list[0]][$list[1]] = 0;
							}
							continue;
						}

						// Look for additional files among WP system files:
						if ( strpos( $scan_dir, ABSPATH .'wp-admin' ) !== false || strpos( $scan_dir, ABSPATH .'wp-includes' ) !== false ) {
							$snapshot['core_unknown'][$full_path] = 0;
						}

						// Look for additional files in the ABSPATH:
						if ( preg_match( '`^'. ABSPATH .'wp-[^/\\\]+\.ph(?:p(?:[34x7]|5\d?)?|t(?:ml)?|ar)$`', $full_path ) ) {
							if ( $full_path != ABSPATH .'wp-config.php' ) {
								$snapshot['core_unknown_root'][$full_path] = 0;
							}
						}

						// Look for hidden PHP scripts:
						if ( $arg['scan_warnhiddenphp'] && $file[0] == '.' && preg_match( '/\.ph(?:p([34x7]|5\d?)?|t(ml)?|ar)$/', $file ) ) {
							$snapshot['core_hidden'][$full_path] = 1;
						}
					}

				// Unreadable file/dir:
				} else {
					if ( $arg['scan_warnunreadable'] ) {
						$snapshot['core_unreadable'][$full_path] = 1;
					}
				}
			}
			closedir( $dh );
		}
   }
}

// =====================================================================
// Write to log the memory usage and execution time.

function nscan_memory_used( $starttime, $startmem, $metrics ) {

	$mem = memory_get_peak_usage( false );
	$ns = $mem - $startmem;

	if ( $metrics == 'hrtime' ) {
		$elapse = number_format( ( $metrics( true ) - $starttime ) / 1000000000, 4 );
	} else {
		$elapse = number_format( $metrics( true ) - $starttime, 4 );
	}

	nscan_log_debug( sprintf(
		__('Process executed in %s seconds and used %s MB of memory (NinjaScanner additional memory: %s MB).', 'ninjascanner' ),
		$elapse,
		number_format_i18n( $mem / 1024 / 1024, 2 ),
		number_format_i18n( $ns / 1024 / 1024, 2 )
	) );
}

// =====================================================================

function nscan_check_wordpress( $lock_status ) {

	global $snapshot;

	$nscan_options = get_option( 'nscan_options' );

	if ( $nscan_options['scan_wpcoreintegrity'] ) {

		$message = __("Checking WordPress core files integrity", 'ninjascanner' );
		nscan_log_info( $message );
		nscan_set_lock_status(
			$lock_status['current_step'],
			'success',
			$message
		);

		if ( _nscan_check_wordpress( $lock_status ) === true ) {
			nscan_log_info( __('Files integrity is O.K', 'ninjascanner') );
		}
		if (! empty( $snapshot['core_unknown'] ) ) {
			nscan_log_warn( sprintf(
				__('Additional/suspicious files: %s', 'ninjascanner'),
				number_format_i18n( count( $snapshot['core_unknown'] ) )
			));
		}

	} else {
		// Skip this step
		$message = __("Skipping WordPress core files integrity check", 'ninjascanner' );
		nscan_log_info( $message );
		$snapshot['skip']['scan_wpcoreintegrity'] = 1;
	}

	// Save snapshot
	file_put_contents( NSCAN_TMP_SNAPSHOT, serialize( $snapshot ) );

	nscan_set_lock_status(
		++$lock_status['current_step'],
		'success',
		$message
	);

}

// ---------------------------------------------------------------------
// Check WordPress core files integrity by downloading it from
// wordpress.org or using its local cached copy.
// The copy will be kept locally until the garbage collector cron job
// deletes it.

function _nscan_check_wordpress( $lock_status ) {

	$nscan_options = get_option( 'nscan_options' );
	global $snapshot, $wp_version, $wp_local_package, $nscan_steps;

	if ( empty( $wp_local_package ) ) {
		$wp_zip = "wordpress-{$wp_version}.zip";
		$wp_zip_url = "https://wordpress.org/{$wp_zip}";
	} else {
		$wp_zip = "wordpress-{$wp_version}-{$wp_local_package}.zip";
		$wp_zip_url = "https://de.wordpress.org/{$wp_zip}";
	}

	// Remove empty/corrupted file, if any
	if ( file_exists( NSCAN_CACHEDIR ."/$wp_zip" ) ) {
		if ( filesize( NSCAN_CACHEDIR ."/$wp_zip" ) < 10000000 ) { // WP is 15MB+
			unlink( NSCAN_CACHEDIR ."/$wp_zip" );
		}
	}

	// Download it if we don't have a copy in our cache:
	if (! file_exists( NSCAN_CACHEDIR ."/$wp_zip" ) ) {

		$message = sprintf( __('Downloading %s from wordpress.org', 'ninjascanner'), $wp_zip );
		nscan_log_debug( $message );
		nscan_set_lock_status(
			$lock_status['current_step'],
			'success',
			$message
		);

		$res = wp_remote_get(
			$wp_zip_url,
			array(
				'stream' => true,
				'filename' => NSCAN_CACHEDIR ."/$wp_zip",
				'timeout' => NSCAN_CURL_TIMEOUT,
				'httpversion' => '1.1' ,
				'user-agent' => 'Mozilla/5.0 (compatible; NinjaScanner/'.
										NSCAN_VERSION .'; WordPress/'. $wp_version . ')',
				'sslverify' => true
			)
		);
		if ( is_wp_error( $res ) ) {
			// Save error:
			$message = sprintf(
				__('%s. Skipping this step', 'ninjascanner'), $res->get_error_message()
			);
			$snapshot['step_error'][ $nscan_steps[ $lock_status['current_step'] ] ] = $message;
			nscan_log_error( $message );
			return false;
		}

		if ( $res['response']['code'] != 200 ) {
			if (file_exists( NSCAN_CACHEDIR ."/$wp_zip" )) {
				unlink( NSCAN_CACHEDIR ."/$wp_zip" );
			}

			nscan_log_warn( sprintf(
				__('HTTP Error %s. Skipping this step, you may try again later', 'ninjascanner'),
				(int)$res['response']['code']
			));
			return false;
		}

	// Use the local copy:
	} else {
		nscan_log_debug(
			sprintf( __('Using local cached copy (%s)', 'ninjascanner'), $wp_zip )
		);
	}

	$zip_files_list = array();
	if ( ( $zip_files_list = nscan_get_zip_files_list(  NSCAN_CACHEDIR ."/$wp_zip" ) ) === false ) {
		// Save error:
		$message = __('Unable to retrieve ZIP files list. Skipping this step', 'ninjascanner');
		$snapshot['step_error'][ $nscan_steps[ $lock_status['current_step'] ] ] = $message;
		nscan_log_error( $message );
		return false;
	}

	$message = __('Checking WordPress files integrity', 'ninjascanner');
	nscan_set_lock_status(
		$lock_status['current_step'],
		'success',
		$message
	);

	// Extract the ZIP
	if ( nscan_extract_archive( NSCAN_CACHEDIR ."/$wp_zip", NSCAN_CACHEDIR ."/$wp_version" ) === false ) {
		// Save error:
		$err = __('Unable to extract ZIP archive. Skipping this step', 'ninjascanner');
		nscan_log_error( $err );
		$snapshot['step_error'][ $nscan_steps[ $lock_status['current_step'] ] ] = $err;
		return false;
	}

	// Select algo:
	if ( empty( $nscan_options['scan_checksum'] ) || $nscan_options['scan_checksum'] == 1 ) {
		$algo = 'md5';
	} elseif ( $nscan_options['scan_checksum'] == 2 ) {
		$algo = 'sha1';
	} else {
		$algo = 'sha256';
	}
	nscan_log_debug( sprintf( __('Using %s algo', 'ninjascanner'), $algo ) );

	// Compare local files with archive files:
	foreach( $zip_files_list as $file => $checksum ) {

		// Don't check bundled themes/plugins, because the blog may use newer versions,
		// but still check the index.php of the themes, plugins & wp-content folders
		// as well as the "Hello_Dolly" plugin:
		if ( $file == 'wp-content/index.php' || $file == 'wp-content/themes/index.php' ) {
			$tmpfile = str_replace( 'wp-content', WP_CONTENT_DIR, $file );

		} elseif ( $file == 'wp-content/plugins/index.php' || $file == 'wp-content/plugins/hello.php' ) {
			$tmpfile = str_replace( 'wp-content/plugins', WP_PLUGIN_DIR, $file );

		} elseif ( strpos( $file, 'wp-content/plugins/' ) !== false || strpos( $file, 'wp-content/themes/' ) !== false ) {
			continue;

		} else {
			$tmpfile = ABSPATH . $file;
		}

		// Make sure the file exists:
		if ( isset( $snapshot['abspath'][$tmpfile] ) ) {
			$local_file = hash_file( $algo, $tmpfile );
			$original_file = hash_file( $algo, NSCAN_CACHEDIR ."/$wp_version/wordpress/$file" );

			// Compare checksums:
			if ( $local_file !== $original_file ) {
				$snapshot['core_failed_checksum'][$tmpfile] = 1;
				nscan_log_warn(
					sprintf( __( 'Checksum mismatch: %s', 'ninjascanner' ), $tmpfile )
				);
				$snapshot['abspath'][$tmpfile]['type'] = 'core';
			} else {
				$snapshot['abspath'][$tmpfile]['v'] = 1;
			}

		}
		// Used to check for additional files uploaded in
		// the wp-admin & wp-includes folders and ABSPATH:
		if ( isset( $snapshot['core_unknown'][$tmpfile] ) ) {
			unset( $snapshot['core_unknown'][$tmpfile] );
		}
		if ( isset( $snapshot['core_unknown_root'][$tmpfile] ) ) {
			unset( $snapshot['core_unknown_root'][$tmpfile] );
		}
	}
	// Remove the extracted files/directories:
	nscan_remove_dir( NSCAN_CACHEDIR ."/$wp_version" );


	// Build the files/folders exclusion list
	$excluded_folders = '';
	if (! empty( $nscan_options['scan_folders'] ) && ! empty( $nscan_options['scan_folders_fic'] ) ) {
		$folders = json_decode( $nscan_options['scan_folders'], true );
		if ( is_array( $folders ) ) {
			foreach( $folders as $folder ) {
				$excluded_folders .= preg_quote( $folder ) . '|';
			}
			$excluded_folders = rtrim( $excluded_folders , '|' );
			nscan_log_debug( __('Creating files/folders exclusion list', 'ninjascanner') );
		}
	}
	// Remove unknow file that are in the exclusion list
	if ( $excluded_folders ) {
		foreach( $snapshot['core_unknown_root'] as $n => $t ) {
			if ( preg_match( "`$excluded_folders`i", $n ) ) {
				unset( $snapshot['core_unknown_root'][ $n ] );
				nscan_log_debug(
					sprintf(
						__('Ignoring unknown file, it is in the exclusion list: %s', 'ninjascanner'),
						$n
					)
				);
			}
		}
		foreach( $snapshot['core_unknown'] as $n => $t ) {
			if ( preg_match( "`$excluded_folders`i", $n ) ) {
				unset( $snapshot['core_unknown'][ $n ] );
				nscan_log_debug(
					sprintf(
						__('Ignoring unknown file, it is in the exclusion list: %s', 'ninjascanner'),
						$n
					)
				);
			}
		}
	}

	if (! empty( $snapshot['core_failed_checksum'] ) ) {
		nscan_log_warn( sprintf(
			__('Total modified core files: %s', 'ninjascanner'),
			count( $snapshot['core_failed_checksum'] )
		));
		return false;
	}
	// Checksums match:
	return true;
}

// =====================================================================
// Return the list of files from a ZIP archive without extracting them.

function nscan_get_zip_files_list( $zip_file ) {

	nscan_log_debug( __('Building files list from ZIP archive', 'ninjascanner') );

	// By default we use ZipArchive, but if it's not available,
	// we fall back to the built-in PclZip library:
	if ( class_exists('ZipArchive') ) {

		$zip = new ZipArchive();
		$zip_files_list = array();

		if ( ( $res = $zip->open( $zip_file ) ) === true ) {
			for ( $i = 0; $i < $zip->numFiles; ++$i ) {
				$stat = $zip->statIndex( $i );
				// Ignore folders:
				if ( substr( $stat['name'], -1 ) == '/' ) {
					continue;
				}
				// Remove the plugin slug + its following slash:
				$file_name =  substr( $stat['name'], strpos( $stat['name'], '/' ) + 1 );
				$zip_files_list[$file_name] = $stat['crc']; // 'crc' is unused since v3.0
				unset($stat);
			}
			$zip->close();

			// Make sure we have something:
			if ( count( $zip_files_list ) < 1 ) {
				nscan_log_error( __('Files list is empty. Skipping this archive', 'ninjascanner') );
				return false;
			}
			// Return the files list:
			return $zip_files_list;
		}

		nscan_log_error( sprintf(
			__('Unable to open ZIP archive (error code: %s)', 'ninjascanner'),
			$res
		));
		// Delete the corrupted zip file
		unlink( $zip_file );
		return false;

	} else {
		// PclZip
		require_once ABSPATH .'wp-admin/includes/class-pclzip.php';

		$zip = new PclZip( $zip_file );
		$zip_files_list = array();

		if ( ( $list = $zip->listContent() ) === 0 ) {
			nscan_log_error( sprintf(
				__('Unable to open ZIP archive (error code: %s)', 'ninjascanner'),
				'PclZip'
			));
			// Delete the corrupted zip file
			unlink( $zip_file );
			return false;
		}
		for ( $i = 0; $i < sizeof( $list ); $i++ ) {
			// Ignore folders:
			if ( substr( $list[$i]['filename'], -1 ) == '/' ) {
				continue;
			}
			// Remove the plugin slug + its following slash:
			$file_name =  substr( $list[$i]['filename'], strpos( $list[$i]['filename'], '/' ) + 1 );
			$zip_files_list[$file_name] = 1;
		}
		// Make sure we have something:
		if ( count( $zip_files_list ) < 1 ) {
			nscan_log_error( __('Files list is empty. Skipping this archive', 'ninjascanner') );
			return false;
		}
		// Return the files list:
		return $zip_files_list;
	}
}

// =====================================================================
// Extract a ZIP archive into the cache folder. Destination folder
// will match the plugin/theme slug.

function nscan_extract_archive( $zip_file, $destination_folder ) {

	if ( is_dir( $destination_folder ) ) {
		// The destination folder exists, let's delete it:
		nscan_remove_dir( $destination_folder );
	}

	if ( mkdir( $destination_folder ) === false ) {
		nscan_log_warn( sprintf(
			__('Cannot create folder %s. Is your filesystem read-only?', 'ninjascanner'),
			$destination_folder
		));
		return false;
	}

	// By default we use ZipArchive, but if it's not available,
	// we fall back to the built-in PclZip library:
	if ( class_exists('ZipArchive') ) {
		$zip = new ZipArchive;
		if ( ( $res = $zip->open( $zip_file ) ) === true ) {
			$zip->extractTo( $destination_folder );
			$zip->close();
			return true;
		}

	} else {
		// PclZip
		require_once ABSPATH .'wp-admin/includes/class-pclzip.php';
		$zip = new PclZip( $zip_file );
		if ( $zip->extract( $destination_folder ) !== 0 ) {
			return true;
		}
	}

	nscan_log_error( sprintf(
		__('Unable to extract ZIP archive (error code: %s)', 'ninjascanner'),
		$res
	));
	// Delete destination folder:
	nscan_remove_dir( $destination_folder );

	return false;
}

// =====================================================================
// Check Google Safe Browsing.

function nscan_check_gsb( $lock_status ) {

	global $snapshot, $wp_version, $nscan_steps;

	$nscan_options = get_option( 'nscan_options' );

	if ( $nscan_options['scan_gsb'] ) {
		$message = __('Checking Google Safe Browsing', 'ninjascanner' );
		nscan_log_info( $message );
		nscan_set_lock_status(
			$lock_status['current_step'],
			'success',
			$message
		);

		$url = '';

		// In a multisite environment, we must check all sites:
		if ( is_multisite() && function_exists( 'get_sites' ) && class_exists( 'WP_Site_Query' ) ) {
			$mysites = get_sites([
				'public'  => 1,
				// <500, Google Safe Browsing usage limit
				'number'  => 499,
				'orderby' => 'registered',
				'order'   => 'ASC'
			]);
			foreach( $mysites as $id => $v ) {
				$site = get_site_url( $mysites[$id]->blog_id );
				if ( $site ) {
					$url .= '{"url": "'. $site .'"},';
				}
			}
			$total = count($mysites);
			$mysites = '';

		// Single site:
		} else {
			$site = home_url('/');
			$url .= '{"url": "'. $site .'"},';
			$total = 1;
		}

		nscan_log_info( sprintf(
			__('Total URL to check: %s', 'ninjascanner'),
			$total
		) );

		// Used for Google referrer restriction:
		$referrer = get_site_url();

		$body = array(
			'body' => '{
				"threatInfo": {
					"threatTypes":      ["MALWARE", "SOCIAL_ENGINEERING"],
					"platformTypes":    ["ANY_PLATFORM"],
					"threatEntryTypes": ["URL"],
					"threatEntries": [
						'. $url .'
					]
				}
			}',
			'headers' => array(
				'content-type' => 'application/json',
				'Referer' => $referrer
			),
			'data_format' => 'body',
			'user-agent' => 'Mozilla/5.0 (compatible; NinjaScanner/'.
								NSCAN_VERSION ."; WordPress/{$wp_version})",
			'timeout' => NSCAN_CURL_TIMEOUT,
			'httpversion' => '1.1' ,
			'sslverify' => true
		);
		$res = wp_remote_post( NSCAN_GSB . "?key={$nscan_options['scan_gsb']}", $body );

		if (! is_wp_error($res) ) {
			$data = json_decode( $res['body'], true );

			// Invalid key:
			if (! empty( $data['error']['message'] ) ) {
				nscan_log_error( $data['error']['message'] );
				$snapshot['step_error'][ $nscan_steps[ $lock_status['current_step'] ] ] = $data['error']['message'];
				goto GSB_SAVE_SNAPSHOT;
			}

			$snapshot['scan_gsb'] = array();
			if (! empty( $data['matches'] ) ) {
				foreach( $data['matches'] as $key ) {
					foreach( $key as $k => $v ) {
						$snapshot['scan_gsb'][$key['threat']['url']] = 1;
					}
				}
			}

			if (! empty( $snapshot['scan_gsb'] ) ) {
				nscan_log_warn( sprintf(
					__('Total blacklisted URL: %s', 'ninjascanner' ),
					count( $snapshot['scan_gsb'] )
				));
			}

		// Unknown error
		} else {
			$err = sprintf(
				__('%s. Cannot check Google Safe Browsing. Try again later', 'ninjascanner'),
				$res->get_error_message()
			);
			nscan_log_error( $err );
			$snapshot['step_error'][ $nscan_steps[ $lock_status['current_step'] ] ] = $err;
		}

	} else {
		$message = __('Skipping Google Safe Browsing: no API key found', 'ninjascanner');
		nscan_log_info( $message );
		$snapshot['skip']['scan_gsb'] = 1;
	}


GSB_SAVE_SNAPSHOT:
	// Save snapshot
	file_put_contents( NSCAN_TMP_SNAPSHOT, serialize( $snapshot ) );

	nscan_set_lock_status(
		++$lock_status['current_step'],
		'success',
		$message
	);
}

// =====================================================================
// Retrieve the list of all plugins (inc. MU and dropins).

function nscan_get_plugins_list( $lock_status ) {

	global $snapshot, $ignored_files;

	$nscan_options = get_option( 'nscan_options' );

	if ( $nscan_options['scan_pluginsintegrity'] ) {
		$message = __('Building plugins list', 'ninjascanner' );
		nscan_log_info( $message );
		nscan_set_lock_status(
			$lock_status['current_step'],
			'success',
			$message
		);

		// Build the list of plugins (slug & version):
		if ( ! function_exists( 'get_plugins' ) ) {
			require_once ABSPATH . 'wp-admin/includes/plugin.php';
		}
		$plugins = get_plugins();
		$nscan_plugins_list = array();
		foreach( $plugins as $k => $v ) {
			if ( $slug = substr( $k, 0, strpos( $k, '/' ) ) ) {
				$nscan_plugins_list['plugins'][$slug] = $v['Version'];
			} else {
				// Ignore 'Hello Dolly', we checked it already with WP core files:
				if ( $v['Name'] != 'Hello Dolly' ) {

					// Don't know what it is. It could be a backdoor,
					// we'll warn the user about it later:
					$snapshot['plugins_unknown'][$k] = $v['Version'];

					nscan_log_warn( sprintf(
						__('Additional/suspicious plugin: %s %s (%s)', 'ninjascanner'),
						$v['Name'], $v['Version'], WP_PLUGIN_DIR . "/$k"
					));
				}
			}
		}

		// Check if there is any MU plugins too:
		$mu_plugins = get_mu_plugins();
		foreach( $mu_plugins as $k => $v ) {

			if (! empty( $ignored_files[ WPMU_PLUGIN_DIR ."/$k"] ) ) {
				// It's in our ignored list, skip it
				continue;
			}

			if ( $slug = substr( $k, 0, strpos( $k, '/' ) ) ) {
				// Plugin with a folder/slug:
				$snapshot['mu_plugins'][$slug] = $v['Version'];

			} else {
				// No folder, just a single PHP script:
				if ( $k == '0-ninjafirewall.php' ) {
					$mu = __FILE__; // Placeholder
					if ( file_exists( WP_PLUGIN_DIR .'/ninjafirewall/lib/loader.php' ) ) {
						$mu = WP_PLUGIN_DIR .'/ninjafirewall/lib/loader.php';
					} elseif ( file_exists( WP_PLUGIN_DIR .'/nfwplus/lib/loader.php' ) ) {
						$mu = WP_PLUGIN_DIR .'/nfwplus/lib/loader.php';
					}
					if ( md5_file( WPMU_PLUGIN_DIR . "/$k" ) === md5_file( $mu ) ) {
						continue;
					}
				}
				$snapshot['mu_plugins'][$k] = $v['Version'];

			}
			nscan_log_warn( sprintf(
				__('mu-plugin found: %s %s (%s)', 'ninjascanner'),
				$v['Name'], $v['Version'], WPMU_PLUGIN_DIR . "/$k"
			));
		}

		$dropins = array(
			'advanced-cache.php', 'db.php', 'db-error.php', 'install.php', 'maintenance.php',
			'object-cache.php', 'sunrise.php', 'blog-deleted.php', 'blog-inactive.php',
			'blog-suspended.php'
		);
		foreach( $dropins as $dropin ) {
			if (! empty( $snapshot['abspath'][WP_CONTENT_DIR ."/$dropin"] ) ) {
				$snapshot['plugins_dropins'][$dropin] = 1;
			}
		}

		if ( empty( $nscan_plugins_list['plugins'] ) ) {
			nscan_log_warn( __('No plugins found', 'ninjascanner') );
			goto PLUGINS_SAVE_SNAPSHOT;
		}

		nscan_log_debug( sprintf(
			__('Total plugins found: %s', 'ninjascanner'),
			count( $nscan_plugins_list['plugins'] )
		));

		// Save list to temp file
		file_put_contents( NSCAN_TMP_LIST, serialize( $nscan_plugins_list ) );

		// Build the files/folders exclusion list
		$excluded_folders = 'readme\.txt|';
		if (! empty( $nscan_options['scan_folders'] ) && ! empty( $nscan_options['scan_folders_fic'] ) ) {
			$folders = json_decode( $nscan_options['scan_folders'], true );
			if ( is_array( $folders ) ) {
				foreach( $folders as $folder ) {
					$excluded_folders .= preg_quote( $folder ) . '|';
				}
				nscan_log_debug( __('Creating files/folders exclusion list', 'ninjascanner') );
			}
		}
		$snapshot['tmp']['excluded_folders'] = rtrim( $excluded_folders , '|' );


	// Skip this step
	} else {
		$message = __('Skipping plugin files integrity check', 'ninjascanner');
		nscan_log_info( $message );
		$snapshot['skip']['scan_pluginsintegrity'] = 1;
		// We want to skip the next step as well
		++$lock_status['current_step'];
	}

PLUGINS_SAVE_SNAPSHOT:
	// Save snapshot
	file_put_contents( NSCAN_TMP_SNAPSHOT, serialize( $snapshot ) );

	nscan_set_lock_status(
		++$lock_status['current_step'],
		'success',
		$message
	);

}

// =====================================================================
// Check integrity of all plugins.

function nscan_check_plugins( $lock_status ) {

	global $snapshot, $ignored_files;

	$nscan_options = get_option( 'nscan_options' );

	$message = __('Checking plugin files integrity', 'ninjascanner' );

	nscan_log_info( $message );
	nscan_set_lock_status(
		$lock_status['current_step'],
		'success',
		$message
	);

	$failed = 0;

	// Fetch files/folders exclusion list
	$excluded_folders = $snapshot['tmp']['excluded_folders'];

	// Retrieve list of plugins
	$nscan_plugins_list = unserialize( file_get_contents( NSCAN_TMP_LIST ) );
	@unlink( NSCAN_TMP_LIST );

	// Let's check their integrity if possible
	// (i.e., if they are available on wordpress.org)
	$unknown_count = 0;
	foreach( $nscan_plugins_list['plugins'] as $slug => $version ) {

		nscan_is_scan_cancelled();

		$msg = "$slug $version";
		nscan_log_debug( $msg );
		nscan_set_lock_status(
			$lock_status['current_step'],
			'success',
			"$message ($msg)"
		);
		// Remove empty file, if any
		if ( file_exists( NSCAN_CACHEDIR ."/plugin_{$slug}.{$version}.zip" ) ) {
			if ( filesize( NSCAN_CACHEDIR ."/plugin_{$slug}.{$version}.zip" ) < 100 ) {
				unlink( NSCAN_CACHEDIR ."/plugin_{$slug}.{$version}.zip" );
			}
		}

		// If we already checked NinjaScanner files integrity, we skip it:
		if ( $nscan_options['scan_ninjaintegrity'] && $slug == 'ninjascanner' ) {
			nscan_log_debug( __('Ignoring NinjaScanner, its integrity was checked already', 'ninjascanner') );
			$snapshot['plugins'][$slug] = array();
			continue;
		}

		// Users can upload their own ZIP in a folder named "local":
		if ( file_exists( NSCAN_LOCAL ."/{$slug}.{$version}.zip" ) ) {
			nscan_log_debug( __('Using user-uploaded local copy', 'ninjascanner') );
			$plugin_zip = NSCAN_LOCAL ."/{$slug}.{$version}.zip";

		// Check if we have a cached copy of the ZIP file:
		} elseif ( file_exists( NSCAN_CACHEDIR ."/plugin_{$slug}.{$version}.zip" ) ) {
			nscan_log_debug( __('Using local copy', 'ninjascanner') );
			$plugin_zip = NSCAN_CACHEDIR ."/plugin_{$slug}.{$version}.zip";

		} else {
			nscan_log_debug( __('Attempting to download it from wordpress.org', 'ninjascanner') );

			if ( nscan_wp_repo_download( $slug, $version, 'plugin', false ) === false ) {
				// Try to download it from the trunk folder instead:
				nscan_log_debug( __('Not found. Attempting to download it from the trunk folder instead', 'ninjascanner') );
				if ( nscan_wp_repo_download( $slug, $version, 'plugin', true ) === false ) {
					// Remove the plugin from the list if we didn't find it in the WP repo:
					unset( $snapshot['plugins'][$slug] );
					$snapshot['plugins_not_found'][$slug] = $version;
					continue;
				}
			}
			$plugin_zip = NSCAN_CACHEDIR ."/plugin_{$slug}.{$version}.zip";
		}

		// Return the ZIP archive list of files:
		$zip_files_list = array();
		if ( ( $zip_files_list = nscan_get_zip_files_list( $plugin_zip ) ) === false ) {
			// Error, try next one:
			continue;
		}

		// Extract the ZIP
		if ( nscan_extract_archive( $plugin_zip, NSCAN_CACHEDIR ."/$slug" ) === false ) {
			// Error, try next one:
			continue;
		}

		// Select algo:
		if ( empty( $nscan_options['scan_checksum'] ) || $nscan_options['scan_checksum'] == 1 ) {
			$algo = 'md5';
		} elseif ( $nscan_options['scan_checksum'] == 2 ) {
			$algo = 'sha1';
		} else {
			$algo = 'sha256';
		}

		nscan_log_debug( sprintf( __('Using %s algo', 'ninjascanner'), $algo ) );

		// Compare local files with archive files:
		foreach( $zip_files_list as $file => $checksum ) {

			// Make sure the file exists and is not on our ignored files list
			if ( file_exists( WP_PLUGIN_DIR ."/$slug/$file" ) && empty( $ignored_files[WP_PLUGIN_DIR ."/$slug/$file"] ) ) {

				$local_file = hash_file( $algo, WP_PLUGIN_DIR ."/$slug/$file" );
				$original_file = hash_file( $algo, NSCAN_CACHEDIR ."/$slug/$slug/$file" ); // NSCAN_CACHEDIR/slug/slug/*

				// Compare checksums:
				if ( $local_file !== $original_file ) {
					$snapshot['plugins'][$slug][$file] = 1;
					nscan_log_warn( sprintf(
						__('Checksum mismatch: %s', 'ninjascanner'),
						WP_PLUGIN_DIR ."/$slug/$file"
					));
					++$failed;
					// Record type, version and slug for the report:
					$snapshot['abspath'][WP_PLUGIN_DIR ."/$slug/$file"]['slug'] = $slug;
					$snapshot['abspath'][WP_PLUGIN_DIR ."/$slug/$file"]['version'] = $version;
					$snapshot['abspath'][WP_PLUGIN_DIR ."/$slug/$file"]['type'] = 'plugin';
				} else {
					// Remove the file from our list if it matches:
					unset( $snapshot['plugins'][$slug][$file] );
					$snapshot['abspath'][WP_PLUGIN_DIR ."/$slug/$file"]['v'] = 2;
				}
			}
		}
		// Remove the extracted files/directories:
		nscan_remove_dir( NSCAN_CACHEDIR ."/$slug" );

		// Look for additional files in the plugins folder
		// unless we didn't scan that folder
		if (! empty( $snapshot['plugins'][$slug] ) ) {
			foreach( $snapshot['plugins'][$slug] as $k => $v ) {
				if ( $excluded_folders && preg_match( "`$excluded_folders`i", WP_PLUGIN_DIR ."/$slug/$k" ) ) {
					// Ignore it, it's in the exclusion list:
					unset( $snapshot['plugins'][$slug][$k] );
					continue;
				}
				if ( $v == 0 ) { ++$unknown_count; }
			}
		}
	}

	if ( $unknown_count ) {
		nscan_log_warn( sprintf(
			__('Additional/suspicious files: %s', 'ninjascanner'),
			$unknown_count
		));
	}

	if ( $failed ) {
		nscan_log_warn( sprintf(
			__('Total modified plugin files: %s', 'ninjascanner'),
			$failed
		));

	} else {
		nscan_log_info( __('Plugin files integrity is O.K', 'ninjascanner') );
	}

	// Save snapshot
	file_put_contents( NSCAN_TMP_SNAPSHOT, serialize( $snapshot ) );

	nscan_set_lock_status(
		++$lock_status['current_step'],
		'success',
		$message
	);
}

// =====================================================================
// Retrieve the list of all themes.

function nscan_get_themes_list( $lock_status ) {

	global $snapshot;

	$nscan_options = get_option( 'nscan_options' );

	if ( $nscan_options['scan_themeseintegrity'] ) {
		$message = __('Building themes list', 'ninjascanner' );
		nscan_log_info( $message );
		nscan_set_lock_status(
			$lock_status['current_step'],
			'success',
			$message
		);

		// Build the list of themes (slug & version):
		if ( ! function_exists( 'wp_get_themes' ) ) {
			require_once ABSPATH . 'wp-includes/theme.php';
		}
		$themes = wp_get_themes();
		$nscan_themes_list = array();
		foreach( $themes as $k => $v ) {
			$nscan_themes_list['themes'][$k] = $v->Version;
		}

		if ( empty( $nscan_themes_list['themes'] ) ) {
			// That should never happened!
			nscan_log_warn( __('No themes found', 'ninjascanner') );
			goto THEMES_SAVE_SNAPSHOT;
		}

		nscan_log_debug( sprintf(
			__('Total themes found: %s', 'ninjascanner'),
			count( $nscan_themes_list['themes'] )
		));

		// Save list to temp file
		file_put_contents( NSCAN_TMP_LIST, serialize( $nscan_themes_list ) );

		// Build the files/folders exclusion list
		$excluded_folders = '';
		if (! empty( $nscan_options['scan_folders'] ) && ! empty( $nscan_options['scan_folders_fic'] ) ) {
			$folders = json_decode( $nscan_options['scan_folders'], true );
			if ( is_array( $folders ) ) {
				foreach( $folders as $folder ) {
					$excluded_folders .= preg_quote( $folder ) . '|';
				}
				$excluded_folders = rtrim( $excluded_folders , '|' );
				nscan_log_debug( __('Creating files/folders exclusion list', 'ninjascanner') );
			}
		}
		$snapshot['tmp']['excluded_folders'] = $excluded_folders;

	// Skip this step
	} else {
		$message = __('Skipping theme files integrity check', 'ninjascanner');
		nscan_log_info( $message );
		$snapshot['skip']['scan_themeseintegrity'] = 1;
		// We want to skip the next step as well
		++$lock_status['current_step'];
	}

THEMES_SAVE_SNAPSHOT:
	// Save snapshot
	file_put_contents( NSCAN_TMP_SNAPSHOT, serialize( $snapshot ) );

	nscan_set_lock_status(
		++$lock_status['current_step'],
		'success',
		$message
	);

}

// =====================================================================
// Check integrity of all themes.

function nscan_check_themes( $lock_status ) {

	global $snapshot, $ignored_files;

	$nscan_options = get_option( 'nscan_options' );

	$message = __('Checking theme files integrity', 'ninjascanner' );

	nscan_log_info( $message );
	nscan_set_lock_status(
		$lock_status['current_step'],
		'success',
		$message
	);

	$failed = 0;

	// Fetch files/folders exclusion list
	$excluded_folders = $snapshot['tmp']['excluded_folders'];

	// Retrieve list of themes
	$nscan_themes_list = unserialize( file_get_contents( NSCAN_TMP_LIST ) );
	@unlink( NSCAN_TMP_LIST );

	// Let's check their integrity if possible
	// (i.e., they are available at wordpress.org)
	$unknown_count = 0;
	foreach( $nscan_themes_list['themes'] as $slug => $version ) {

		nscan_is_scan_cancelled();

		$msg = "$slug $version";
		nscan_log_debug( $msg );
		nscan_set_lock_status(
			$lock_status['current_step'],
			'success',
			"$message ($msg)"
		);

		// Remove empty file, if any
		if ( file_exists( NSCAN_CACHEDIR ."/theme_{$slug}.{$version}.zip" ) ) {
			if ( filesize( NSCAN_CACHEDIR ."/theme_{$slug}.{$version}.zip" ) < 100 ) {
				unlink( NSCAN_CACHEDIR ."/theme_{$slug}.{$version}.zip" );
			}
		}

		// Users can upload their own ZIP in a folder named "local":
		if ( file_exists( NSCAN_LOCAL ."/{$slug}.{$version}.zip" ) ) {
			nscan_log_debug( __('Using user-uploaded local copy', 'ninjascanner') );
			$theme_zip = NSCAN_LOCAL ."/{$slug}.{$version}.zip";

		// Check if we have a cached copy of the ZIP file:
		} elseif ( file_exists( NSCAN_CACHEDIR ."/theme_{$slug}.{$version}.zip" ) ) {
			nscan_log_debug( __('Using local copy', 'ninjascanner') );
			$theme_zip = NSCAN_CACHEDIR ."/theme_{$slug}.{$version}.zip";

		} else {
			nscan_log_debug( __('Attempting to download it from wordpress.org', 'ninjascanner') );

			if ( nscan_wp_repo_download( $slug, $version, 'theme', false ) === false ) {
				// Remove the theme from the list if we didn't find it in the WP repo:
				unset( $snapshot['themes'][$slug] );
				$snapshot['themes_not_found'][$slug] = $version;
				continue;
			}
			$theme_zip = NSCAN_CACHEDIR ."/theme_{$slug}.{$version}.zip";
		}

		// Return the ZIP archive list of files:
		$zip_files_list = array();
		if ( ( $zip_files_list = nscan_get_zip_files_list( $theme_zip ) ) === false ) {
			// Error, try next one:
			continue;
		}

		// Extract the ZIP
		if ( nscan_extract_archive( $theme_zip, NSCAN_CACHEDIR ."/$slug" ) === false ) {
			// Error, try next one:
			continue;
		}

		// Select algo:
		if ( empty( $nscan_options['scan_checksum'] ) || $nscan_options['scan_checksum'] == 1 ) {
			$algo = 'md5';
		} elseif ( $nscan_options['scan_checksum'] == 2 ) {
			$algo = 'sha1';
		} else {
			$algo = 'sha256';
		}

		nscan_log_debug( sprintf( __('Using %s algo', 'ninjascanner'), $algo ) );

		// Compare local files with archive files:
		foreach( $zip_files_list as $file => $checksum ) {

			// Make sure the file exists and is not on our ignored files list
			if ( file_exists( WP_CONTENT_DIR ."/themes/$slug/$file" ) && empty( $ignored_files[WP_CONTENT_DIR ."/themes/$slug/$file"] ) ) {
				$local_file = hash_file( $algo, WP_CONTENT_DIR ."/themes/$slug/$file" );
				$original_file = hash_file( $algo, NSCAN_CACHEDIR ."/$slug/$slug/$file" ); // NSCAN_CACHEDIR/slug/slug/*

				// Compare checksums:
				if ( $local_file !== $original_file ) {
					$snapshot['themes'][$slug][$file] = 1;
					nscan_log_warn( sprintf(
						__('Checksum mismatch: %s', 'ninjascanner'),
						WP_CONTENT_DIR ."/themes/$slug/$file"
					));
					++$failed;
					// Record type, version and slug for the report:
					$snapshot['abspath'][WP_CONTENT_DIR ."/themes/$slug/$file"]['slug'] = $slug;
					$snapshot['abspath'][WP_CONTENT_DIR ."/themes/$slug/$file"]['version'] = $version;
					$snapshot['abspath'][WP_CONTENT_DIR ."/themes/$slug/$file"]['type'] = 'theme';
				} else {
					// Remove the file from our list if it matches:
					unset( $snapshot['themes'][$slug][$file] );
					$snapshot['abspath'][WP_CONTENT_DIR ."/themes/$slug/$file"]['v'] = 2;
				}
			}
		}
		// Remove the extracted files/directories:
		nscan_remove_dir( NSCAN_CACHEDIR ."/$slug" );

		// Look for additional files in the themes folder
		// unless we didn't scan that folder
		if (! empty( $snapshot['themes'][$slug] ) ) {
			foreach( $snapshot['themes'][$slug] as $k => $v ) {
				if ( $excluded_folders && preg_match( "`$excluded_folders`i", WP_CONTENT_DIR ."/themes/$slug/$k" ) ) {
					// Ignore it, it's in the exclusion list:
					unset( $snapshot['themes'][$slug][$k] );
					continue;
				}
				if ( $v == 0 ) { ++$unknown_count; }
			}
		}
	}

	if ( $unknown_count ) {
		nscan_log_warn( sprintf(
			__('Additional/suspicious files: %s', 'ninjascanner'),
			$unknown_count
		));
	}

	if ( $failed ) {
		nscan_log_warn( sprintf(
			__('Total modified theme files: %s', 'ninjascanner'),
			$failed
		));

	} else {
		nscan_log_info( __('Theme files integrity is O.K', 'ninjascanner') );
	}

	// Save snapshot
	file_put_contents( NSCAN_TMP_SNAPSHOT, serialize( $snapshot ) );

	nscan_set_lock_status(
		++$lock_status['current_step'],
		'success',
		$message
	);
}

// =====================================================================
// Download a plugin or theme ZIP file from the wordpress.org repo
// and save it to the cache folder. The copy will be kept locally
// until NinjaScanner's garbage collector cron job deletes it.
// If it is a plugin and the operation failed (404 Not Found), this
// function is called once again in an attempt to downloaded the file
// from the trunk folder instead (some developers may not tag their plugin).

function nscan_wp_repo_download( $slug, $version, $type, $trunk = false ) {

	global $wp_version;

	if ( $type == 'plugin' ) {
		// Plugin URL:
		if ( $trunk ) {
			$url = NSCAN_PLUGINS_URL ."{$slug}.zip";
		} else {
			$url = NSCAN_PLUGINS_URL ."{$slug}.{$version}.zip";
		}

	} else {
		// Theme URL:
		$url = NSCAN_THEMES_URL ."{$slug}.{$version}.zip";
	}

	$res = wp_remote_get(
		$url,
		array(
			'stream' => true,
			'filename' => NSCAN_CACHEDIR ."/{$type}_{$slug}.{$version}.zip",
			'timeout' => NSCAN_CURL_TIMEOUT,
			'httpversion' => '1.1' ,
			'user-agent' => 'Mozilla/5.0 (compatible; NinjaScanner/'.
									NSCAN_VERSION .'; WordPress/'. $wp_version . ')',
			'sslverify' => true
		)
	);

	if (! is_wp_error( $res ) ) {
		if ( $res['response']['code'] == 200 ) {
			return true;
		}

		if (file_exists( NSCAN_CACHEDIR ."/{$type}_{$slug}.{$version}.zip" )) {
			unlink( NSCAN_CACHEDIR ."/{$type}_{$slug}.{$version}.zip" );
		}
		if ( $trunk || $type == 'theme' ) {
			// Probably not available in wordpress.org repo, ignore it:
			nscan_log_warn( sprintf(
				__('HTTP Error %s. Skipping %s %s, it may not be available in the repo', 'ninjascanner'),
				(int)$res['response']['code'],
				$slug,
				$version
			));
		}
		return false;
	}

	// Unknown error:
	nscan_log_error( sprintf(
		__('%s. Skipping it. You may try again later', 'ninjascanner'),
		$res->get_error_message()
	));
	return false;
}

// =====================================================================
// Check NinjaScanner's files integrity by downloading its checksum
// hashes - or using the local cached copy. In case of mismatch,
// refuse to run (users can still bypass this by disabling the integrity
// checker from the settings page).

function nscan_check_ninjascanner_integrity() {

	global $wp_version;
	global $snapshot;
	$nscan_hashes = array();

	// Do we have a local cached version?
	if ( file_exists( NSCAN_HASHFILE ) ) {
		nscan_log_debug( __('Using local cached version of checksums', 'ninjascanner') );
		$nscan_hashes = json_decode( file_get_contents( NSCAN_HASHFILE ), true );
		// Make sure we have what we are expecting:
		if ( empty( $nscan_hashes['checksums']['ninjascanner/lib/constants.php'] ) ) {
			nscan_log_warn( __('Decoded hashes seem corrupted. Deleting local cached version', 'ninjascanner') );
			// Delete the file:
			unlink( NSCAN_HASHFILE );
			$nscan_hashes = array();
		}
	}

	// NinjaScanner's wordpress.og repo URL:
	$url = sprintf( NSCAN_SVN_PLUGINS, 'ninjascanner',	NSCAN_VERSION ) .'/checksum.txt';

	// Download them:
	if ( empty( $nscan_hashes ) ) {
		nscan_log_debug( __('Downloading checksums', 'ninjascanner') );
		$res = wp_remote_get(
			$url,
			array(
				'timeout' => NSCAN_CURL_TIMEOUT,
				'httpversion' => '1.1' ,
				'user-agent' => 'Mozilla/5.0 (compatible; NinjaScanner/'.
										NSCAN_VERSION .'; WordPress/'. $wp_version . ')',
				'sslverify' => true
			)
		);
		if ( is_wp_error( $res ) ) {
			nscan_log_error(
				sprintf( __('%s. Skipping this step', 'ninjascanner'), $res->get_error_message() )
			);
			// Don't return false, the server may be down. We'll attempt
			// to check the files again while checking all plugins integrity:
			return -1;
		}
		// Decode the content:
		$nscan_hashes = json_decode( $res['body'], true );
		// Make sure we have what we are expecting:
		if ( empty( $nscan_hashes['checksums']['ninjascanner/lib/constants.php'] ) ) {
			$message = __('Fatal error: NinjaScanner files integrity check: Decoded hashes seem corrupted. Aborting.', 'ninjascanner');
			$snapshot['error'] = $message;
			nscan_log_error( $message );
			return false;
		}
		// Save it to disk:
		file_put_contents( NSCAN_HASHFILE, $res['body'] );
	}

	// Loop through the array and compare hashes:
	$failed = 0;
	$missing = 0;
	foreach( $nscan_hashes['checksums'] as $file => $checksum ) {

		// Use WP_PLUGIN_DIR as user may have changed the path:
		$tmpfile = WP_PLUGIN_DIR . "/$file";
		if ( file_exists( $tmpfile ) ) {
			// Checksum does not match?
			if ( hash_file( 'sha256', $tmpfile ) !== $checksum ) {
				++$failed;
				nscan_log_warn(
					sprintf( __( 'Checksum mismatch: %s', 'ninjascanner' ), $tmpfile )
				);
			}
		} else {
			// Missing file:
			++$missing;
			nscan_log_warn(
				sprintf( __( 'Missing file: %s', 'ninjascanner' ), $tmpfile )
			);
		}
	}

	if ( $failed || $missing ) {
		$message = sprintf(
			__('Fatal error: Some NinjaScanner files have been modified (%s) or are missing (%s). Please reinstall NinjaScanner or disable NinjaScanner files integrity checker. Aborting.', 'ninjascanner'),
			"x$failed",
			"x$missing"
		);
		$snapshot['error'] = $message;
		nscan_log_error( $message );
		// Delete cached version:
		unlink( NSCAN_HASHFILE );
		return false;
	}

	// Checksums match:
	return true;
}

// =====================================================================
// Removed files/folders from the array depending of the user-defined
// exclusion lists (based on names and file size).

function nscan_apply_exclusion( $buffer, $log = 1, $verified = 1 ) {

	$nscan_options = get_option( 'nscan_options' );
	$count = 0;

	if ( $log ) {
		nscan_log_debug( __('Checking user-defined exclusion lists', 'ninjascanner') );
	}

	// Build the extensions exclusion list (case insensitive):
	$excluded_extensions = '';
	if (! empty( $nscan_options['scan_extensions'] ) ) {
		$extensions = json_decode( $nscan_options['scan_extensions'], true );
		if ( is_array( $extensions ) ) {
			foreach( $extensions as $extension ) {
				$excluded_extensions .= preg_quote( $extension ) . '|';
			}
			$excluded_extensions = '\.(?:'. rtrim( $excluded_extensions , '|' ) . ')$';
			if ( $log ) {
				nscan_log_debug( __('Creating extensions exclusion list', 'ninjascanner') );
			}
		}
	}
	// Build the files/folders exclusion list
	$excluded_folders = '';
	if (! empty( $nscan_options['scan_folders'] ) ) {
		$folders = json_decode( $nscan_options['scan_folders'], true );
		if ( is_array( $folders ) ) {
			foreach( $folders as $folder ) {
				$excluded_folders .= preg_quote( $folder ) . '|';
			}
			$excluded_folders = rtrim( $excluded_folders , '|' );
			if ( $log ) {
				nscan_log_debug( __('Creating files/folders exclusion list', 'ninjascanner') );
			}
		}
	}
	// Filesize limit:
	$file_size = 0;
	if (! empty( $nscan_options['scan_size'] ) ) {
		$file_size = $nscan_options['scan_size'] * 1024;
		if ( $log ) {
			nscan_log_debug( sprintf(
				__('Limiting search to files smaller than %s bytes', 'ninjascanner' ),
				number_format_i18n( $file_size )
			) );
		}
	}

	// Apply the two exclusion lists to the array:
	foreach( $buffer as $file => $values ) {

		// Used for binaries scan
		if (! $verified && ! empty( $values['v'] ) ) {
			unset( $buffer[$file] );
			++$count;
			continue;
		}

		if ( $excluded_extensions && preg_match( "`$excluded_extensions`i", $file ) ) {
			// Remove files from list:
			unset( $buffer[$file] );
			++$count;
			continue;
		}
		if ( $excluded_folders && preg_match( "`$excluded_folders`", $file ) ) {
			// Remove files from list:
			unset( $buffer[$file] );
			++$count;
			continue;
		}

		// Since v3.x, $values[1] may not exist if the file
		// was located in a excluded root folder
		if (! isset( $values[1] ) || ( $file_size && $values[1] > $file_size ) ) {
			// Too big, exclude it too
			unset( $buffer[$file] );
			++$count;
			continue;
		}
	}

	if ( $count && $log ) {
		nscan_log_debug( sprintf(
			__('Files ignored based on user-defined exclusion lists: %s', 'ninjascanner' ),
			$count
		));
	}

	// Return the buffer:
	return $buffer;
}

// =====================================================================
// Compare the current and previous snapshots for modifications (added,
// deleted and modified files).

function nscan_compare_snapshots( $lock_status ) {

	global $snapshot;

	$nscan_options = get_option( 'nscan_options' );

	$message = __('Comparing previous and current file snapshots', 'ninjascanner' );

	if (! file_exists( NSCAN_OLD_SNAPSHOT ) ) {
		nscan_log_info( __('Skipping snapshots comparison, no older files shapshot found', 'ninjascanner') );

	} elseif (! empty( $nscan_options['scan_warnfilechanged'] ) ) {

		nscan_log_info( $message );
		nscan_set_lock_status(
			$lock_status['current_step'],
			'success',
			$message
		);

		$previous_snapshot = array();
		$old_snapshot = unserialize( file_get_contents( NSCAN_OLD_SNAPSHOT ) );

		if ( empty( $old_snapshot['abspath'] ) ) {
			nscan_log_warn( __('Old snapshot file seems corrupted. Skipping this step', 'ninjascanner') );
			goto FILESNAPSHOTSAVE;
		}

		// Removed excluded files (based on user-exclusion lists)
		$current_snapshot = nscan_apply_exclusion( $snapshot['abspath'] );
		$previous_snapshot = nscan_apply_exclusion( $old_snapshot['abspath'], 0 );

		$count = 0;
		foreach( $current_snapshot as $file => $stat ) {
			// File didn't exist when the previous snapshot was taken:
			if (! isset( $previous_snapshot[$file] ) ) {
				$snapshot['snapshot']['added_files'][$file] = 1;
				++$count;
				continue;
			}
			// File was changed:
			if ( $previous_snapshot[$file][0] != $stat[0] ) {
				$snapshot['snapshot']['mismatched_files'][$file] = 1;
				++$count;
			}
			// Remove it from the list:
			unset( $previous_snapshot[$file] );
		}

		foreach( $previous_snapshot as $file => $stat ) {
			// File was removed:
			$snapshot['snapshot']['deleted_files'][$file] = 1;
			++$count;
		}

		if (! empty( $snapshot['snapshot']['added_files'] ) ) {
			nscan_log_warn( sprintf(
				__('Total additional files: %s', 'ninjascanner' ),
				count( $snapshot['snapshot']['added_files'] )
			));
		}
		if (! empty( $snapshot['snapshot']['mismatched_files'] ) ) {
			nscan_log_warn( sprintf(
				__('Total modified files: %s', 'ninjascanner' ),
				count( $snapshot['snapshot']['mismatched_files'] )
			));
		}
		if (! empty( $snapshot['snapshot']['deleted_files'] ) ) {
			nscan_log_warn( sprintf(
				__('Total deleted files: %s', 'ninjascanner' ),
				count( $snapshot['snapshot']['deleted_files'] )
			));
		}

		if (! $count ) {
			nscan_log_info( __('Previous and current snapshots match', 'ninjascanner') );
		}

	} else {
		$message = __('Skipping snapshots comparison', 'ninjascanner');
		nscan_log_info( $message );
		$snapshot['skip']['scan_warnfilechanged'] = 1;
	}

FILESNAPSHOTSAVE:
	// Save snapshot
	file_put_contents( NSCAN_TMP_SNAPSHOT, serialize( $snapshot ) );

	nscan_set_lock_status(
		++$lock_status['current_step'],
		'success',
		$message
	);
}

// =====================================================================
// Compare the current and previous database snapshots for modifications
// (added, deleted and modified posts and pages).

function nscan_compare_db_snapshots( $lock_status ) {

	global $snapshot;

	$nscan_options = get_option( 'nscan_options' );

	$message = __('Comparing previous and current database snapshots', 'ninjascanner' );

	if (! file_exists( NSCAN_OLD_SNAPSHOT ) ) {
		nscan_log_info( __('Skipping snapshots comparison, no older database shapshot found', 'ninjascanner') );

	} elseif (! empty( $nscan_options['scan_warndbchanged'] ) ) {

		nscan_log_info( $message );
		nscan_set_lock_status(
			$lock_status['current_step'],
			'success',
			$message
		);

		$old_snapshot = unserialize( file_get_contents( NSCAN_OLD_SNAPSHOT ) );

		if ( empty( $old_snapshot['abspath'] ) ) {
			nscan_log_warn( __('Old snapshot file seems corrupted. Skipping this step', 'ninjascanner') );
			goto DBNAPSHOTSAVE;

		} elseif (! isset( $old_snapshot['posts'] ) && ! isset( $old_snapshot['pages'] ) ) {
			nscan_log_info( __('Skipping snapshots comparison, no older database shapshot found', 'ninjascanner') );
			goto DBNAPSHOTSAVE;
		}

		$count = 0;

		// Posts:
		foreach( $snapshot['posts'] as $id => $val ) {
			// Post didn't exist when the previous snapshot was taken:
			if (! isset( $old_snapshot['posts'][$id] ) ) {
				$snapshot['snapshot']['added_posts'][$id] = $val['permalink'];
				++$count;
				continue;
			}
			// Post was changed:
			if ( $old_snapshot['posts'][$id]['hash'] != $val['hash'] ) {
				$snapshot['snapshot']['mismatched_posts'][$id] = $val['permalink'];
				++$count;
			}
			// Remove it from the list:
			unset( $old_snapshot['posts'][$id] );
		}
		// Make sur its not empty:
		if ( is_array( $old_snapshot['posts'] ) ) {
			foreach( $old_snapshot['posts'] as $id => $val ) {
				// Post was removed:
				$snapshot['snapshot']['deleted_posts'][$id] = $val['permalink'];
				++$count;
			}
		}
		if (! empty( $snapshot['snapshot']['added_posts'] ) ) {
			nscan_log_warn( sprintf(
				__('Total additional posts: %s', 'ninjascanner' ),
				count( $snapshot['snapshot']['added_posts'] )
			));
		}
		if (! empty( $snapshot['snapshot']['mismatched_posts'] ) ) {
			nscan_log_warn( sprintf(
				__('Total modified posts: %s', 'ninjascanner' ),
				count( $snapshot['snapshot']['mismatched_posts'] )
			));
		}
		if (! empty( $snapshot['snapshot']['deleted_posts'] ) ) {
			nscan_log_warn( sprintf(
				__('Total deleted posts: %s', 'ninjascanner' ),
				count( $snapshot['snapshot']['deleted_posts'] )
			));
		}

		// Pages:
		foreach( $snapshot['pages'] as $id => $val ) {
			// Page didn't exist when the previous snapshot was taken:
			if (! isset( $old_snapshot['pages'][$id] ) ) {
				$snapshot['snapshot']['added_pages'][$id] = $val['permalink'];
				++$count;
				continue;
			}
			// Page was changed:
			if ( $old_snapshot['pages'][$id]['hash'] != $val['hash'] ) {
				$snapshot['snapshot']['mismatched_pages'][$id] = $val['permalink'];
				++$count;
			}
			// Remove it from the list:
			unset( $old_snapshot['pages'][$id] );
		}
		// Make sur its not empty:
		if ( is_array( $old_snapshot['pages'] ) ) {
			foreach( $old_snapshot['pages'] as $id => $val ) {
				// Page was removed:
				$snapshot['snapshot']['deleted_pages'][$id] = $val['permalink'];
				++$count;
			}
		}
		if (! empty( $snapshot['snapshot']['added_pages'] ) ) {
			nscan_log_warn( sprintf(
				__('Total additional pages: %s', 'ninjascanner' ),
				count( $snapshot['snapshot']['added_pages'] )
			));
		}
		if (! empty( $snapshot['snapshot']['mismatched_pages'] ) ) {
			nscan_log_warn( sprintf(
				__('Total modified pages: %s', 'ninjascanner' ),
				count( $snapshot['snapshot']['mismatched_pages'] )
			));
		}
		if (! empty( $snapshot['snapshot']['deleted_pages'] ) ) {
			nscan_log_warn( sprintf(
				__('Total deleted pages: %s', 'ninjascanner' ),
				count( $snapshot['snapshot']['deleted_pages'] )
			));
		}

		if (! $count ) {
			nscan_log_info( __('Previous and current snapshots match', 'ninjascanner') );
		}

	} else {
		$message = __('Skipping snapshots comparison', 'ninjascanner');
		nscan_log_info( $message );
		$snapshot['skip']['scan_warndbchanged'] = 1;
	}

DBNAPSHOTSAVE:
	// Save snapshot
	file_put_contents( NSCAN_TMP_SNAPSHOT, serialize( $snapshot ) );

	nscan_set_lock_status(
		++$lock_status['current_step'],
		'success',
		$message
	);
}

// =====================================================================
// Build signatures and exclusion list for the malware scan.

function nscan_setup_antimalware( $lock_status ) {

	global $snapshot;

	// Check if we just started a new scan
	// or if we are already running
	if ( file_exists( NSCAN_FILES2CHECK ) ) {
		nscan_set_lock_status(
			++$lock_status['current_step'],
			'success',
			$lock_status['message']
		);
		return;
	}

	$nscan_options = get_option( 'nscan_options' );

	$scan_signatures = array();
	$scan_signatures = json_decode( $nscan_options['scan_signatures'], true );

	$message = __('Running malware scanner', 'ninjascanner');

	if (! empty( $scan_signatures ) ) {

		nscan_log_info( $message );
		nscan_set_lock_status(
			$lock_status['current_step'],
			'success',
			$message
		);

		// Removed excluded files (based on user-exclusion lists)
		nscan_log_info( __('Building the list of files to check', 'ninjascanner') );
		$current_snapshot = array();
		$current_snapshot = nscan_apply_exclusion( $snapshot['abspath'] );
		// Save it
		file_put_contents( NSCAN_FILES2CHECK, serialize( $current_snapshot ) );

		// Check all signatures
		$signatures_list = array();
		nscan_log_info( __('Retrieving signatures lists', 'ninjascanner') );
		foreach( $scan_signatures as $sig ) {
			$tmp_list = array();
			// Built-in LMD + NinjaScanner signatures list
			if ( $sig == 'lmd' ) {
				// Download it:
				if ( nscan_download_signatures( $lock_status ) === false ) {
					continue;
				}
				// Verify signatures and return the list
				if ( ( $tmp_list = nscan_verify_signatures( NSCAN_SIGNATURES ) ) === false ) {
					continue;
				}
			} else {
				nscan_log_debug( sprintf(
					__('Checking user-defined signatures list (%s)', 'ninjascanner'), $sig
				));
				// Verify signatures and return the list:
				if ( ( $tmp_list = nscan_verify_signatures( $sig ) ) === false ) {
					continue;
				}
			}
			if ( $tmp_list ) {
				// Concatenate arrays
				$signatures_list += $tmp_list;
			}
		}
		// Saved compiled signatures (for 2 hours in the db)
		if ( $signatures_list ) {
			set_transient( 'nscan_temp_sigs', base64_encode( serialize( $signatures_list ) ), 7200 );
		} else {
			nscan_log_error( __('No valid signatures found', 'ninjascanner') );
			$snapshot['skip']['scan_antimalware'] = 1;
			// We want to skip the next step as well
			++$lock_status['current_step'];
		}

	} else {
		$message = __('Skipping malware scan', 'ninjascanner');
		nscan_log_info( $message );
		$snapshot['skip']['scan_antimalware'] = 1;
		// We want to skip the next step as well
		++$lock_status['current_step'];
	}

	// Save snapshot
	file_put_contents( NSCAN_TMP_SNAPSHOT, serialize( $snapshot ) );

	nscan_set_lock_status(
		++$lock_status['current_step'],
		'success',
		$message
	);
}

// =====================================================================
// Scan files for malware.

function nscan_run_antimalware( $lock_status ) {

	global $snapshot;

	// Check if we still have some files to scan
	if (! file_exists( NSCAN_FILES2CHECK ) ) {
		$message = $lock_status['message'];
		goto ANTIMALWARESAVE; // goto power!
	}

	$files2check = unserialize( file_get_contents( NSCAN_FILES2CHECK ) );
	if ( empty( $files2check ) ) {
		$message = __('Files list array seems corrupted', 'ninjascanner');
		nscan_log_error( $message );
		goto ANTIMALWARESAVE; // goto power!
	}

	$nscan_temp_sigs = get_transient( 'nscan_temp_sigs' );
	if ( $nscan_temp_sigs === false ) {
		$message = __('No valid signatures found', 'ninjascanner');
		nscan_log_error( $message );
		goto ANTIMALWARESAVE; // goto power!
	}

	$signatures = unserialize( base64_decode( $nscan_temp_sigs ) );
	if ( empty( $signatures ) ) {
		nscan_log_error( __('Signatures list is empty', 'ninjascanner') );
	}

	$message = __('Running malware scanner', 'ninjascanner');
	$msg = __('items scanned:', 'ninjascanner');

	if (! empty( $snapshot['tmp']['scanstats'] ) ) {
		$stat = explode( ':', $snapshot['tmp']['scanstats'] );
		$total_scanned = $stat[0];
		$total_to_scan =  $stat[1];
	} else {
		$total_scanned = 0;
		$total_to_scan = count( $files2check );
	}

	$log_interval = 0;
	$start = time();

	foreach( $files2check as $file => $v ) {

		if ( time() - $start > NSCAN_PHPTIMEOUT ) {
			break;
		}

		if ( $log_interval > 10 ) {
			nscan_set_lock_status(
				$lock_status['current_step'],
				'success',
				"$message ($msg $total_scanned/$total_to_scan)"
			);
			$log_interval = 1;
			nscan_is_scan_cancelled();

		} else {
			++$log_interval;
		}
		++$total_scanned;

		if ( isset( $v['v'] ) && $v['v'] == 1 ) {
			// Don't scan core files that were verified already:
			unset( $files2check[$file] );
			continue;
		}

		if (! file_exists( $file ) ) {
			// The file may have just been deleted (e.g., temp file etc):
			nscan_log_warn( sprintf(
				__('File does not exist, ignoring it: %s', 'ninjascanner'), $file
			));
			unset( $files2check[$file] );
			continue;
		}

		// Clear the file from our buffer so that we could
		// restart where the scan left off in case or error/crash:
		unset( $files2check[$file] );

		if ( ( $content = file_get_contents( $file ) ) !== false ) {
			foreach ( $signatures as $name => $sig ) {

				// Don't scan verified plugin (v==2) & theme (v==3) files,
				// unless the signature requests it:
				if ( isset( $v['v'] ) && $v['v'] != $name[4] ) {
					continue;
				}

				// Regex signature:
				if ( $name[1] == 'R' ) {
					if ( preg_match( "`$sig`", $content ) ) {
						$snapshot['infected_files'][$file] = $name;
						nscan_log_warn( sprintf(
							__('Potentially unsafe files: %s', 'ninjascanner'), $file
						));
						break;
					}
				// Simple signature:
				} else {
					if ( strpos( $content, $sig ) !== false ) {
						$snapshot['infected_files'][$file] = $name;
						nscan_log_warn( sprintf(
							__('Potentially unsafe files: %s', 'ninjascanner'), $file
						));
						break;
					}
				}
				// Cancel scan request
				if ( file_exists( NSCAN_CANCEL ) ) {
					exit;
				}
			}

		} else {
			nscan_log_error( sprintf(
				__('Cannot open %s, skipping it', 'ninjascanner'), $file
			));
		}
	}

	// Temporarily stop and fork another process
	if (! empty( $files2check ) ) {
		$snapshot['tmp']['scanstats'] = "$total_scanned:$total_to_scan";
		file_put_contents( NSCAN_TMP_SNAPSHOT, serialize( $snapshot ) );
		file_put_contents( NSCAN_FILES2CHECK, serialize( $files2check ) );
		nscan_log_info( sprintf(
			__('Scanned files: %s/%s', 'ninjascanner'),
			number_format_i18n( $total_scanned ),
			number_format_i18n( $total_to_scan )
		) );
		return;
	}

	if (! empty( $snapshot['infected_files'] ) ) {
		nscan_log_warn( sprintf(
			__('Total potentially unsafe files: %s', 'ninjascanner'),
			count( $snapshot['infected_files'] )
		));
	} else {
		nscan_log_info( sprintf(
			__('No suspicious file detected (%s files checked)', 'ninjascanner'),
			number_format_i18n( $total_scanned )
		) );
	}

ANTIMALWARESAVE:
	file_put_contents( NSCAN_TMP_SNAPSHOT, serialize( $snapshot ) );

	nscan_set_lock_status(
		++$lock_status['current_step'],
		'success',
		$message
	);
}

// =====================================================================
// Download built-in  signatures list, or used the cached version
// if not older than one hour:

function nscan_download_signatures( $lock_status ) {

	global $snapshot, $nscan_steps;
	$nscan_options = get_option( 'nscan_options' );

	nscan_log_debug( __('Checking built-in signatures list', 'ninjascanner') );

	// Check it we have a cached version:
	if ( file_exists( NSCAN_SIGNATURES ) ) {
		if ( time() - filemtime( NSCAN_SIGNATURES ) < 600 ) {
			// Use it:
			nscan_log_debug( __('Using local copy', 'ninjascanner') );
			return;
		} else {
			// Too old, delete it:
			unlink( NSCAN_SIGNATURES );
			nscan_log_debug( __('Local copy is too old, deleting it', 'ninjascanner') );
		}
	}

	// Download the latest available version:
	nscan_log_debug( __('Downloading the latest version', 'ninjascanner') );
	global $wp_version;

	// Prepare the POST request:
	$data = array();
	$request_string = array(
		'body' => array(
			'action'	=> 'signatures',
			's' => 1,
			'cache_id' => sha1( home_url() )
		),
		'user-agent' => 'Mozilla/5.0 (compatible; NinjaScanner/'.
							NSCAN_VERSION ."; WordPress/{$wp_version})",
		'timeout' => NSCAN_CURL_TIMEOUT,
		'httpversion' => '1.1' ,
		'sslverify' => true
	);
	if ( isset( $nscan_options['key'] ) ) {
		// Premium users only:
		$request_string['body']['key'] = $nscan_options['key'];
		$request_string['body']['host'] = @strtolower( $_SERVER['HTTP_HOST'] );
	}

	// POST the request:
	$res = wp_remote_post( NSCAN_SIGNATURES_URL, $request_string);

	if (! is_wp_error($res) ) {
		if ( $res['response']['code'] == 200 ) {
			// Fetch the array:
			$data = json_decode( $res['body'], true );

			if (! empty( $data['exp'] ) ) {
				$nscan_options['exp'] = $data['exp'];
				update_option( 'nscan_options', $nscan_options );
			}

			// Make sure we have some signatures (a sig starts with '{', e.g., '{HEX}xxxxx'):
			if ( empty( $data['sig'] ) || $data['sig'][0] != '{' ) {
				if (! isset( $data['err'] ) ) { $data['err'] = 0; }
				$err = sprintf(
					__('The signatures list is either corrupted or empty. Try again later (error %s)', 'ninjascanner'),
					(int) $data['err']
				);
				nscan_log_warn( $err );
				$snapshot['step_error'][ $nscan_steps[ $lock_status['current_step'] ] ] = $err;
				return false;
			}

			// Verify the digital signature:
			if ( function_exists( 'openssl_pkey_get_public') && function_exists( 'openssl_verify' ) ) {
				nscan_log_debug( __('Verifying digital signature with public key', 'ninjascanner') );
				$public_key = rtrim( file_get_contents( __DIR__ .'/sign.pub' ) );
				$pubkeyid = openssl_pkey_get_public( $public_key );
				$verify = openssl_verify( trim( $data['sig'] ), base64_decode( $data['s'] ), $pubkeyid, OPENSSL_ALGO_SHA256);
				if ( $verify != 1 ) {
					$err = __('The digital signature is not correct. Aborting update, rules may have been tampered with.', 'ninjascanner');
					nscan_log_warn( $err );
					$snapshot['step_error'][ $nscan_steps[ $lock_status['current_step'] ] ] = $err;
					return false;
				}
			}

			// Save the signatures to the cache folder:
			file_put_contents( NSCAN_SIGNATURES, $data['sig'] );
			return true;

		} else {
			// HTTP error:
			$err = sprintf(
				__('HTTP Error %s. Cannot download signatures list. Try again later', 'ninjascanner'),
				(int)$res['response']['code']
			);
			nscan_log_warn( $err );
			$snapshot['step_error'][ $nscan_steps[ $lock_status['current_step'] ] ] = $err;
			return false;
		}
	}

	// Unknown error:
	$err = sprintf(
		__('%s. Cannot download built-in signatures list. Try again later', 'ninjascanner'),
		$res->get_error_message()
	);
	nscan_log_error( $err );
	$snapshot['step_error'][ $nscan_steps[ $lock_status['current_step'] ] ] = $err;
	return false;
}

// =====================================================================
// Read and verify the signatures list (built-in + user-defined),
// and return them as an array.

function nscan_verify_signatures( $file ) {

	// File must exists:
	if (! file_exists( $file ) ) {
		nscan_log_error( sprintf(
			__('Cannot find %s, skipping it', 'ninjascanner'), $file
		));
		return false;
	}

	$fh = fopen( $file, 'r' );
	if (! $fh ) {
		nscan_log_error( sprintf(
			__('Cannot open/read %s, skipping it', 'ninjascanner'), $file
		));
		return false;
	}

	nscan_log_debug( sprintf(
		__('Verifying signatures', 'ninjascanner'), $file
	));

	$tmp_signatures = array();
	$signatures = array();
	while (! feof( $fh ) ) {
		$line = fgets( $fh );
		$tmp_signatures = explode ( ':', rtrim( $line ) );
		unset($line);
		// Make sure we have what we are looking for:
		if (! empty( $tmp_signatures[3] ) && preg_match( '/^{[HR]EX\d?}/', $tmp_signatures[0] ) ) {
			// Decode hex-encoded signatures:
			if ( $res = nscan_hex2str( $tmp_signatures[3], $tmp_signatures[0] ) ) {
				$signatures[$tmp_signatures[0]] = $res;
			}
		}
		unset($tmp_signatures);
	}
	fclose( $fh );

	if (! empty( $signatures ) ) {
		nscan_log_debug( sprintf(
			__('Verified signatures: %s', 'ninjascanner'), count( $signatures )
		));
		return $signatures;
	}

	// Error:
	nscan_log_warn( __('No valid signatures found in that file, skipping it.', 'ninjascanner') );
	return false;
}

// =====================================================================
// Decode and test the hex-encoded signatures, including user-defined
// signatures. Signatures with a syntax error are ignored.

function nscan_hex2str( $hex, $type ) {

    $str = '';
    for ( $i = 0; $i < strlen( $hex ); $i += 2 ) {
		 $str .= chr( hexdec( substr( $hex, $i, 2 ) ) );
	 }
	 if ( preg_match( '/^{REX/', $type ) ) {
		$str = str_replace( '`', '\x60', $str );
		// Check regex validity:
		if ( preg_match("`$str`", 'foobar') === FALSE ) {
			nscan_log_error( sprintf(
				__('REX signature syntax error, skipping it: %s', 'ninjascanner'), $type
			));
			return false;
		}
	 } elseif ( preg_match( '/^{HEX/', $type ) ) {
		// Check signature validity (hex numbers only):
		if ( preg_match( '`[^a-f0-9]`i', $hex ) ) {
			nscan_log_error( sprintf(
				__('HEX signature syntax error, skipping it: %s', 'ninjascanner'), $type
			));
			return false;
		}
	}
	// OK:
	return $str;
}

// =====================================================================
// Scan for binary files (MZ/PE/NE and ELF formats)

function nscan_check_binaries( $lock_status ) {

	global $snapshot, $wp_version;

	$nscan_options = get_option( 'nscan_options' );

	if ( $nscan_options['scan_warnbinary'] ) {
		$message = __('Searching for binary files', 'ninjascanner' );
		nscan_log_info( $message );
		nscan_set_lock_status(
			$lock_status['current_step'],
			'success',
			$message
		);

		$files = array();
		$files = nscan_apply_exclusion( $snapshot['abspath'], 0, 0 );

		foreach( $files as $file => $arr ) {
			$data = file_get_contents( $file, false, null, 0, 4 );
			// We only look for ELF, PE/NE/MZ headers:
			if (preg_match('`^(?:\x7F\x45\x4C\x46|\x4D\x5A)`', $data) ) {
				$snapshot['core_binary'][$file] = 1;
			}
		}

		if (! empty( $snapshot['core_binary'] ) ) {
			nscan_log_warn( sprintf(
				__('Executable files found: %s', 'ninjascanner'),
				number_format_i18n( count( $snapshot['core_binary'] ) )
			));
		} else {
			nscan_log_info( __('No binary file found', 'ninjascanner' ));
		}

	} else {
		$message = __('Skipping binary files scan', 'ninjascanner');
		nscan_log_info( $message );
		$snapshot['skip']['scan_warnbinary'] = 1; // useless ?
	}

	// Save snapshot
	file_put_contents( NSCAN_TMP_SNAPSHOT, serialize( $snapshot ) );

	nscan_set_lock_status(
		++$lock_status['current_step'],
		'success',
		$message
	);
}

// =====================================================================
// Various tests (Note: tests can be disabled in the wp-config.php
// by using their corresponding constant).

function nscan_various_checks( $lock_status ) {

	global $snapshot;

	$nscan_options = get_option( 'nscan_options' );

	$message = __('Performing various checks', 'ninjascanner' );

	nscan_log_info( $message );
	nscan_set_lock_status(
		$lock_status['current_step'],
		'success',
		$message
	);

	if (! defined('NS_SKIP_GHOSTADMIN' ) ) {
		global $wpdb;
		$user_1 = $wpdb->get_results(
			"SELECT {$wpdb->base_prefix}users.ID,{$wpdb->base_prefix}users.user_login,{$wpdb->base_prefix}users.user_pass,{$wpdb->base_prefix}users.user_nicename,{$wpdb->base_prefix}users.user_email,{$wpdb->base_prefix}users.user_registered,{$wpdb->base_prefix}users.display_name
			FROM {$wpdb->base_prefix}users
			INNER JOIN {$wpdb->base_prefix}usermeta
			ON ( {$wpdb->base_prefix}users.ID = {$wpdb->base_prefix}usermeta.user_id )
			WHERE 1=1
			AND ( ( ( {$wpdb->base_prefix}usermeta.meta_key = '{$wpdb->prefix}capabilities'
			AND {$wpdb->base_prefix}usermeta.meta_value LIKE '%\"administrator\"%' ) ) )"
		);
		$user_2 = get_users(
			array( 'role' => 'administrator',
				'fields' => array(
					'ID', 'user_login', 'user_nicename',
					'user_email', 'user_registered', 'display_name'
				)
			)
		);
		if ( count( $user_1) > count( $user_2 ) ) {
			foreach( $user_1 as $num => $user ) {
				if ( isset( $user_2[$num]->ID ) ) {
					unset( $user_1[$num] );
					continue;
				}
			}
			$snapshot['various']['ghost_admin'] = json_encode( $user_1 );
			nscan_log_warn( sprintf(
				_n('Found %s ghost admin user',
					'Found %s ghost admin users',
					count( $user_1 ), 'ninjascanner'
				),
				count( $user_1 )
			));
		}
	}

	if (! defined('NS_SKIP_SSHKEY' ) ) {
		if ( @file_exists( $key = dirname( @$_SERVER['DOCUMENT_ROOT']) .'/.ssh/authorized_keys') ||
			@file_exists( $key = @$_SERVER['DOCUMENT_ROOT'] .'/.ssh/authorized_keys') ) {

			$snapshot['various']['ssh_key'][$key] = 1;
		}
		// For backward compatibility, authorized_keys2 can still be used although deprecated since 2001:
		if ( @file_exists( $key = dirname( @$_SERVER['DOCUMENT_ROOT']) .'/.ssh/authorized_keys2') ||
			@file_exists( $key = @$_SERVER['DOCUMENT_ROOT'] .'/.ssh/authorized_keys2') ) {

			$snapshot['various']['ssh_key'][$key] = 1;
		}
		if (! empty( $snapshot['various']['ssh_key'] ) ) {
			nscan_log_warn( sprintf(
				_n('Found %s SSH key in user home folder',
					'Found %s SSH keys in user home folder',
					count( $snapshot['various']['ssh_key'] ), 'ninjascanner'
				),
				count( $snapshot['various']['ssh_key'] )
			));
		}
	}

	if (! defined('NS_SKIP_WPREGISTRATION' ) ) {
		$default_role = get_option( 'default_role' );
		$users_can_register = get_option( 'users_can_register' );
		if ( $default_role == 'administrator' ) {
			if (! empty( $users_can_register ) ) {
				// Critical
				$snapshot['various']['membership'] = 2;
				nscan_log_warn( __('All New Registered users have administrator role', 'ninjascanner') );
			} else {
				// Important
				$snapshot['various']['membership'] = 1;
				nscan_log_warn( __('New User Default Role is set to "administrator"', 'ninjascanner') );
			}
		}
	}

	if (! defined('NS_SKIP_WPUSERROLES' ) ) {
		$admin_only_cap = array(
			'activate_plugins', 'create_users', 'delete_plugins', 'delete_themes',
			'delete_users', 'edit_files', 'edit_plugins', 'edit_theme_options',
			'edit_themes', 'edit_users', 'export', 'import', 'install_plugins',
			'install_themes', 'list_users', 'manage_options', 'promote_users',
			'remove_users', 'switch_themes', 'update_core', 'update_plugins',
			'update_themes', 'edit_dashboard', 'customize',	'delete_site'
		);
		$exclusion_list = array(
			'shop_manager' => array(
				'slug'	=> WP_PLUGIN_DIR .'/woocommerce/woocommerce.php',
				'caps'	=>	array( 'edit_users', 'export', 'import', 'list_users',
								'edit_theme_options' )
			)
		);
		include_once ABSPATH .'wp-admin/includes/plugin.php';

		// Fetch user_roles:
		global $wpdb;
		$user_roles = get_option("{$wpdb->base_prefix}user_roles");

		foreach ( $user_roles as $user => $cap ) {
			if ( $user != 'administrator' ) {
				foreach( $cap['capabilities'] as $k => $v ) {
					if (! empty( $v ) && in_array( $k, $admin_only_cap ) ) {

						// Check the exclusion list:
						if (! empty( $exclusion_list[$user] ) ) {
							if ( file_exists( $exclusion_list[$user]['slug'] ) ) {
								if ( in_array( $k, $exclusion_list[$user]['caps'] ) ) {
									// Don't warn about this one:
									continue;
								}
							}
						}

						$snapshot['various']['user_roles'][$user][] = $k;
					}
				}
				if (! empty( $snapshot['various']['user_roles'][$user] ) ) {
					nscan_log_warn( sprintf( __('Found user roles with administrator capabilities: %s', 'ninjascanner'), $user ) );
				}
			}
		}
	}

	// Save snapshot
	file_put_contents( NSCAN_TMP_SNAPSHOT, serialize( $snapshot ) );

	nscan_set_lock_status(
		++$lock_status['current_step'],
		'success',
		$message
	);
}

// =====================================================================
// EOF