<?php

if ( ! class_exists( 'bwps_filecheck' ) ) {

	class bwps_filecheck extends bit51_bwps {

		private $maxMemory = 0;
		private $startMem = 0;

	
		/**
		 * Initialize file checker
		 *
		 * Initializes file checker object, runs file checker on schedule and adds admin warning (if applicable)
		 *
		 **/
		function __construct() {
		
			global $bwpsoptions;
			
			//only exececute if it has been more than 24 hours or the check has never occured and file checking is enabled.
			if ( $bwpsoptions['id_fileenabled'] == 1 && defined( 'BWPS_FILECHECK' ) && BWPS_FILECHECK === true && ( $bwpsoptions['id_filechecktime'] == '' || $bwpsoptions['id_filechecktime'] < ( current_time( 'timestamp' ) - 86400 ) ) ) {

				$this->execute_filecheck();
			
			}
			
			//add action for admin warning if enabled 
			if ( isset( $_GET['bit51_view_logs'] ) || $bwpsoptions['id_filedisplayerror'] == 1 || ( isset( $_POST['bwps_page'] ) ) && $_POST['bwps_page'] == 'intrusiondetection_1' ) {
				add_action( 'admin_init', array( $this, 'warning' ) );
			}
			
		}
		
		/**
		 * Check file list
		 *
		 * Checks if given file should be included in file check based on exclude/include options
		 *
		 * @param string $file path of file to check from site root
		 * @return bool true if file should be checked false if not
		 *
		 **/
		function checkFile ( $file ) {
		
			global $bwpsoptions;
			
			//get file list from last check
			$list = $bwpsoptions['id_specialfile'];
			
			//assume not a directory and not checked
			$flag = false;
			
			//if list is empty return true
			if ( trim( $list ) != '' ) {
			
				$list = explode( "\n", $list );
				
			} else {
			
				//if empty include list we include nothing. If empty exclude list we include everything
				if ( $bwpsoptions['id_fileincex'] == 1 ) {
			
					return true;
					
				} else {
				
					return false;
					
				}
				
			}
			
			//compare file to list
			
			foreach ( $list as $item ) {

				$item = trim ( $item );
			
				//$file is a directory
				if ( is_dir( ABSPATH . $file ) ) {
					
					if ( strcmp( $file, $item ) === 0 ) {
						$flag = true;		
					}
				
				} else { //$file is a file
				
					if ( strpos( $item , '.' ) === 0) { //list item is a file extension
					
						if ( strcmp( '.' . end ( explode( '.' , $file ) ), $item ) == 0 ) {
							$flag = true;
						 }
				
					} else { //list item is a single file

						if ( strcmp( $item, $file ) == 0 ) {
							$flag = true;
						}
				
					}
					
				}
				
			}
			
			if ( $bwpsoptions['id_fileincex'] == 1 ) {
			
				if ( $flag == true ) { //if exclude reverse
					return false;
				} else {
					return true;
				}
			
			} else { //return flag 
			
				if ( is_dir( ABSPATH . $file ) ) {
					
					if ( $flag == true ) { //if exclude reverse
						return false;
					} else {
						return true;
					}
					
				} else {
				
					return $flag;
					
				}
				
			}
		
		}
		
		/**
		 * Executes filecheck
		 *
		 * Executes file checking for all operations
		 *
		 * @param bool $auto[optional] is this an automatic check
		 *
		 **/
		function execute_filecheck( $auto = true ) {
		
			global $wpdb, $bwpsoptions, $logid;

			//set base memory
			$this->startMem = @memory_get_usage();
			$this->maxMemory = $this->startMem;
			
			//get old file list
			if ( is_multisite() ) {
					
				switch_to_blog( 1 );
					
				$logItems = maybe_unserialize( get_option( 'bwps_file_log' ) );
					
				restore_current_blog();
					
			} else {
					
				$logItems = maybe_unserialize( get_option( 'bwps_file_log' ) );
						
			}
			
			//if there are no old files old file list is an empty array
			if ( $logItems === false ) {
			
				$logItems = array();
			
			} 
			
			$currItems = $this->scanfiles(); //scan current files
			
			$added = @array_diff_assoc( $currItems, $logItems ); //files added
			$removed = @array_diff_assoc( $logItems, $currItems ); //files deleted
			$compcurrent = @array_diff_key( $currItems, $added ); //remove all added files from current filelist
			$complog = @array_diff_key( $logItems, $removed );  //remove all deleted files from old file list
			$changed = array(); //array of changed files
			
			//compare file hashes and mod dates
			foreach ( $compcurrent as $currfile => $currattr) {
			
				if ( array_key_exists( $currfile, $complog ) ) {
				
					//if attributes differ added to changed files array
					if ( strcmp( $currattr['mod_date'], $complog[$currfile]['mod_date'] ) != 0 || strcmp( $currattr['hash'], $complog[$currfile]['hash'] ) != 0 ) {
						$changed[$currfile]['hash'] = $currattr['hash'];
						$changed[$currfile]['mod_date'] = $currattr['mod_date'];
					}
				
				}
			
			}
			
			//get count of changes
			$addcount = sizeof( $added );
			$removecount = sizeof( $removed );
			$changecount = sizeof( $changed );
			
			//create single array of all changes
			$combined = array(
				'added' => $added,
				'removed' => $removed,
				'changed' => $changed
			);
			
			//save current files to log
			//Get the options
			if ( is_multisite() ) {
					
				switch_to_blog( 1 );
					
				update_option( 'bwps_file_log', serialize( $currItems ) );
					
				restore_current_blog();
					
			} else {
					
				update_option( 'bwps_file_log', serialize( $currItems ) );
						
			}
			
			//log check to database
			$wpdb->insert(
				$wpdb->base_prefix . 'bwps_log',
				array(
					'type' => '3',
					'timestamp' => current_time( 'timestamp' ),
					'host' => '',
					'user' => 0,
					'url' => '',
					'referrer' => '',
					'data' => serialize( $combined )
				)
			);
			
			$logid = $wpdb->insert_id;
			
			//if not the first check and files have changed warn about changes
			if ( $bwpsoptions['id_filechecktime'] != '' ) {
			
				if ( $addcount != 0 || $removecount != 0 || $changecount != 0 ) {
			
					//Update the right options
					if ( is_multisite() ) {
					
						switch_to_blog( 1 );
					
						update_option( 'bwps_intrusion_warning', 1 );
					
						restore_current_blog();
					
					} else {
					
						update_option( 'bwps_intrusion_warning', 1 );
						
					}
				
					if ( $bwpsoptions['id_fileemailnotify'] == 1 ) {
						$this->fileemail();
					}
				
				}

				//get new max memory
				$newMax = @memory_get_peak_usage();
				if ( $newMax > $this->maxMemory ) {
					$this->maxMemory = $newMax;
				}

				//log memory usage
				$wpdb->update(
					$wpdb->base_prefix . 'bwps_log',
					array(
						'mem_used' => ( $this->maxMemory - $this->startMem )
					),
					array(
						'id' => $logid
					)
				);
				
			}
				
			//set latest check time
			$bwpsoptions['id_filechecktime'] = current_time( 'timestamp' );
				
			//Update the right options
			if ( is_multisite() ) {
						
				switch_to_blog( 1 );
						
				update_option( $this->primarysettings, $bwpsoptions );
					
				restore_current_blog();
						
			} else {
						
				update_option( $this->primarysettings, $bwpsoptions );
				
			}
		
		}
		
		/**
		 * Email report
		 *
		 * Sends a report to site admin email address if changes have been detected
		 *
		 **/
		function fileemail() {

			global $logid, $bwpsoptions;
			
			//Get the right email address.
			if ( is_email( $bwpsoptions['id_fileemailaddress'] ) ) {
				
				$toaddress = $bwpsoptions['id_fileemailaddress'];
			
			} else {
			
				$toaddress = get_site_option( 'admin_email' );
				
			}
		
			//create all headers and subject
			$to = $toaddress;
			$headers = 'From: ' . get_option( 'blogname' ) . ' <' . $to . '>' . PHP_EOL;
			$subject = '[' . get_option( 'siteurl' ) . '] ' . __( 'WordPress File Change Warning', $this->hook ) . ' ' . date( 'l, F jS, Y \a\\t g:i a e', current_time( 'timestamp' ) );

			//create message
			$message = '<p>' . __('<p>A file (or files) on your site at ', $this->hook ) . ' ' . get_option( 'siteurl' ) . __( ' have been changed. Please review the report below to verify changes are not the result of a compromise.', $this->hook ) . '</p>';
			$message .= $this->getdetails( $logid, true ); //get report
			
			add_filter( 'wp_mail_content_type', create_function( '', 'return "text/html";' ) ); //send as html

			if ( function_exists( 'wp_mail' ) ) {
				wp_mail( $to, $subject, $message, $headers ); //send message
			}
		
		}
		
		/**
		 * Get Report Details
		 *
		 * Returns details of all changed files found in given report
		 *
		 * @param string $id integer ID of report desired
		 * @param bool $email[optional] is this to be displayed in email
		 * @return string report details
		 *
		 **/
		function getdetails( $id, $email = false ) {
		
			global $wpdb;
			
			//get the change array
			$changes = $wpdb->get_results( "SELECT * FROM `" . $wpdb->base_prefix . "bwps_log` WHERE id=" . absint( $id ) . " ORDER BY timestamp DESC;", ARRAY_A );
			
			if ( $changes == null ) {
				return false;
			}

			//get new max memory
			$newMax = @memory_get_peak_usage();
			if ( $newMax > $this->maxMemory ) {
				$this->maxMemory = $newMax;
			}
			
			$data = maybe_unserialize( $changes[0]['data'] );
				
			//seperate array by category
			$added = $data['added'];
			$removed = $data['removed'];
			$changed = $data['changed'];			
			$report = '<strong>' . __( 'Scan Time:', $this->hook ) . '</strong> ' . date( 'l, F jS g:i a e', $changes[0]['timestamp'] ) . "<br />" . PHP_EOL;
			$report .= '<strong>' . __( 'Files Added:', $this->hook ) . '</strong> ' . sizeof( $added ) . "<br />" . PHP_EOL;
			$report .= '<strong>' . __( 'Files Deleted:', $this->hook ) . '</strong> ' . sizeof( $removed ) . "<br />" . PHP_EOL;
			$report .= '<strong>' . __( 'Files Modified:', $this->hook ) . '</strong> ' . sizeof( $changed ) . "<br />" . PHP_EOL;
			$report .= '<strong>' . __( 'Memory Used:', $this->hook ) . '</strong> ' . round( ( (  $this->maxMemory - $this->startMem ) / 1000000 ), 2 ) . " MB<br />" . PHP_EOL;
		
			if ( $email == true ) {
					
				$report .= '<h4>' . __( 'Files Added', $this->hook ) . '</h4>';
				$report .= '<table border="1" style="width: 100%; text-align: center;">' . PHP_EOL;
				$report .= '<tr>' . PHP_EOL;
				$report .= '<th>' . __( 'File', $this->hook ) . '</th>' . PHP_EOL;
				$report .= '<th>' . __( 'Modified', $this->hook ) . '</th>' . PHP_EOL;
				$report .= '<th>' . __( 'File Hash', $this->hook ) . '</th>' . PHP_EOL;
				$report .= '</tr>' . PHP_EOL;
				if ( isset( $added ) && is_array( $added ) && sizeof( $added > 0 ) ) {
					foreach ( $added as $item => $attr ) { 
						$report .= '<tr>' . PHP_EOL;
						$report .= '<td>' . $item . '</td>' . PHP_EOL;
						$report .= '<td>' . date( 'l F jS, Y \a\t g:i a e', $attr['mod_date'] ) . '</td>' . PHP_EOL;
						$report .= '<td>' . $attr['hash'] . '</td>' . PHP_EOL;
						$report .= '</tr>' . PHP_EOL;
					}
				} else {
					$report .= '<tr>' . PHP_EOL;
					$report .= '<td colspan="3">' . __( 'No files were added.', $this->hook ) . '</td>' . PHP_EOL;
					$report .= '</tr>' . PHP_EOL;
				}
				$report .= '</table>' . PHP_EOL;
			
				$report .= '<h4>' . __( 'Files Deleted', $this->hook ) . '</h4>';
				$report .= '<table border="1" style="width: 100%; text-align: center;">' . PHP_EOL;
				$report .= '<tr>' . PHP_EOL;
				$report .= '<th>' . __( 'File', $this->hook ) . '</th>' . PHP_EOL;
				$report .= '<th>' . __( 'Modified', $this->hook ) . '</th>' . PHP_EOL;
				$report .= '<th>' . __( 'File Hash', $this->hook ) . '</th>' . PHP_EOL;
				$report .= '</tr>' . PHP_EOL;
				if ( isset( $removed ) && is_array( $removed ) && sizeof( $removed > 0 ) ) {
					foreach ( $removed as $item => $attr ) { 
						$report .= '<tr>' . PHP_EOL;
						$report .= '<td>' . $item . '</td>' . PHP_EOL;
						$report .= '<td>' . date( 'l F jS, Y \a\t g:i a e', $attr['mod_date'] ) . '</td>' . PHP_EOL;
						$report .= '<td>' . $attr['hash'] . '</td>' . PHP_EOL;
						$report .= '</tr>' . PHP_EOL;
					}
				} else {
					$report .= '<tr>' . PHP_EOL;
					$report .= '<td colspan="3">' . __( 'No files were removed.', $this->hook ) . '</td>' . PHP_EOL;
					$report .= '</tr>' . PHP_EOL;
				}
				$report .= '</table>' . PHP_EOL;
			
				$report .= '<h4>' . __( 'Files Modified', $this->hook ) . '</h4>';
				$report .= '<table border="1" style="width: 100%; text-align: center;">' . PHP_EOL;
				$report .= '<tr>' . PHP_EOL;
				$report .= '<th>' . __( 'File', $this->hook ) . '</th>' . PHP_EOL;
				$report .= '<th>' . __( 'Modified', $this->hook ) . '</th>' . PHP_EOL;
				$report .= '<th>' . __( 'File Hash', $this->hook ) . '</th>' . PHP_EOL;
				$report .= '</tr>' . PHP_EOL;
				if ( isset( $changed ) && is_array( $changed ) && sizeof( $changed > 0 ) ) {
					foreach ( $changed as $item => $attr ) { 
						$report .= '<tr>' . PHP_EOL;
						$report .= '<td>' . $item . '</td>' . PHP_EOL;
						$report .= '<td>' . date( 'l F jS, Y \a\t g:i a e', $attr['mod_date'] ) . '</td>' . PHP_EOL;
						$report .= '<td>' . $attr['hash'] . '</td>' . PHP_EOL;
						$report .= '</tr>' . PHP_EOL;
					}
				} else {
					$report .= '<tr>' . PHP_EOL;
					$report .= '<td colspan="3">' . __( 'No files were changed.', $this->hook ) . '</td>' . PHP_EOL;
					$report .= '</tr>' . PHP_EOL;
				}
				$report .= '</table>' . PHP_EOL;
			
				return $report;
				
			} else {
			
				echo $report;
			
				$log_details_added_table = new log_details_added_table( $id );
				$log_details_added_table->prepare_items();
				$log_details_added_table->display();
				
				$log_details_removed_table = new log_details_removed_table( $id );
				$log_details_removed_table->prepare_items();
				$log_details_removed_table->display();
				
				$log_details_modified_table = new log_details_modified_table( $id );
				$log_details_modified_table->prepare_items();
				$log_details_modified_table->display();
			
			}
		
		}
		
		/**
		 * Scans all files in a given path
		 * 
		 * Scans all files in a given path and returns an array of filename, mod_date, and file hash
		 *
		 * @param string $path[optional] path to scan, defaults to WordPress root
		 * @return array array of files found and their information
		 *
		 **/
		function scanfiles( $path = '' ) {
			
			global $bwpsoptions;
			
			$tz = get_option( 'gmt_offset' ) * 60 * 60;

            $data = array();

			if ( $dirHandle = @opendir( ABSPATH . $path ) ) { //get the directory
			
				while ( ( $item = readdir( $dirHandle ) ) !== false ) { // loop through dirs
					
					if ( $item != '.' && $item != '..' ) { //don't scan parent/etc

						$relname = $path . $item;
                        
						$absname = ABSPATH . $relname;
						
						if ( $this->checkFile( $relname ) == true ) { //make sure the user wants this file scanned
						
							if ( is_dir( $absname ) && filetype( $absname ) == 'dir' ) { //if directory scan it
							
								$data = array_merge( $data, $this->scanfiles( $relname . '/' ) );
								
							} else { //is file so add to array

								$data[$relname] = array();
								$data[$relname]['mod_date'] = @filemtime( $absname ) + $tz;
								$data[$relname]['hash'] = @md5_file( $absname );
							
							}
						
						}
						
					}
					
				}   
				
				@closedir( $dirHandle ); //close the directory we're working with
                        
			} 
			
			return $data; // return the files we found in this dir
			
		}
		
		/**
		 * Display admin warning
		 *
		 * Displays a warning to adminstrators when file changes have been detected
		 *
		 **/
		function warning() {
		
			global $blog_id; //get the current blog id
			
			if ( ( is_multisite() && ( $blog_id != 1 || ! current_user_can( 'manage_network_options' ) ) ) || ! current_user_can( 'activate_plugins' )  ) { //only display to network admin if in multisite
				return;
			}
		
			//if there is a warning to display
			if ( get_option( 'bwps_intrusion_warning' ) == 1 ) {
			
				if ( ! function_exists( 'bit51_filecheck_warning' ) ) {
			
					function bit51_filecheck_warning(){
				
						global $plugname;
						global $plughook;
						global $plugopts;
						$adminurl = is_multisite() ? admin_url() . 'network/' : admin_url();
					
					    echo '<div class="error">
				       <p>' . __( 'Better WP Security has noticed a change to some files in your WordPress installation. Please review the logs to make sure your system has not been compromised.', $plughook ) . '</p> <p><input type="button" class="button " value="' . __( 'View Logs', $plughook ) . '" onclick="document.location.href=\'?bit51_view_logs=yes&_wpnonce=' .  wp_create_nonce('bit51-nag') . '\';">  <input type="button" class="button " value="' . __('Dismiss Warning', $plughook) . '" onclick="document.location.href=\'' . $adminurl . 'admin.php?bit51_dismiss_warning=yes&_wpnonce=' .  wp_create_nonce( 'bit51-nag' ) . '\';"></p>
					    </div>';
				    
					}
				
				}
				
				//put the warning in the right spot
				if ( is_multisite() ) {
					add_action( 'network_admin_notices', 'bit51_filecheck_warning' ); //register notification
				} else {
					add_action( 'admin_notices', 'bit51_filecheck_warning' ); //register notification
				}
				
			}
			
			//if they've clicked a button hide the notice
			if ( ( isset( $_GET['bit51_view_logs'] ) || isset( $_GET['bit51_dismiss_warning'] ) ) && wp_verify_nonce( $_REQUEST['_wpnonce'], 'bit51-nag' ) ) {
				
				//Get the options
				if ( is_multisite() ) {
						
					switch_to_blog( 1 );
						
					delete_option( 'bwps_intrusion_warning' );
						
					restore_current_blog();
						
				} else {
						
					delete_option( 'bwps_intrusion_warning' );
							
				}
				
				//take them back to where they started
				if ( isset( $_GET['bit51_dismiss_warning'] ) ) {				
					wp_redirect( $_SERVER['HTTP_REFERER'], 302 );
				}
				
				//take them to the correct logs page
				if ( isset( $_GET['bit51_view_logs'] ) ) {
					if ( is_multisite() ) {
						wp_redirect( admin_url() . 'network/admin.php?page=better-wp-security-logs#file-change', 302 );
					} else {
						wp_redirect( admin_url() . 'admin.php?page=better-wp-security-logs#file-change', 302 );
					}
				}
				
			}
		
		}
	
	}

}