<?php

namespace WPSecurityNinja\Plugin;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}



/**
 * Wf_Sn_Visitor_log.
 *
 * @author  Lars Koudal <admin@wpsecurityninja.com>
 * @since   v0.0.1
 * @version v1.0.0    Tuesday, April 18th, 2023.
 * @global
 */
class Wf_Sn_Visitor_log {


	/**
	 * init.
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.0    Friday, May 13th, 2022.
	 * @access  public static
	 * @return  void
	 */
	public static function init() {
		add_action( 'admin_enqueue_scripts', array( __NAMESPACE__ . '\\Wf_Sn_Visitor_log', 'enqueue_scripts' ) );
		add_action( 'wp_ajax_secnin_get_visitors', array( __NAMESPACE__ . '\\Wf_Sn_Visitor_log', 'do_ajax_return_latest_visitors' ) );
		add_action( 'wp_ajax_secnin_vl_banip', array( __NAMESPACE__ . '\\Wf_Sn_Visitor_log', 'do_ajax_banip' ) );
		add_action( 'wp_ajax_secnin_get_visitor_data', array( __NAMESPACE__ . '\\Wf_Sn_Visitor_log', 'ajax_get_visitor_data' ) );
	}

	/**
	 * URLs to be ignored when viewing the visitor log
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.0    Wednesday, November 23rd, 2022.
	 * @access  public static
	 * @return  array
	 */
	public static function return_filterable_urls() {
		static $clean_list = null;

		if ( null === $clean_list ) {
			global $wpdb;
			$default_ignore = array(
				'/favicon.ico',
				'/robots.txt',
				'/?doing_wp_cron',
				'/?kinsta-monitor',
				'&mainwpsignature=',
				'/wordfence_lh=',
				'/wp-json/',
			);
			$ignore_list    = apply_filters( 'securityninja_visitorlog_filter_url', $default_ignore );

			$clean_list = array();
			foreach ( $ignore_list as $il ) {
				$clean_list[] = $wpdb->remove_placeholder_escape( esc_sql( $il ) );
			}
		}

		return $clean_list;
	}

	/**
	 * Returns a row in HTML format ready to be displayed.
	 *
	 * @param object $visitor The visitor object.
	 * @param array  $fmtargs Formatting arguments.
	 * @return string|boolean HTML string or false if visitor is invalid.
	 */
	public static function format_line( $visitor, $fmtargs = array() ) {
		$defaults                 = array(
			'marknew'         => true,
			'current_user_ip' => false,
		);
		$secnin_visitor_log_nonce = wp_create_nonce( 'secnin_visitor_log' );
		$args                     = wp_parse_args( $fmtargs, $defaults );

		$current_user_country = '';
		$ipcol_output         = $visitor->ip;
		$geolocate_ip         = \WPSecurityNinja\Plugin\SN_Geolocation::geolocate_ip( $visitor->ip, true );

		if ( $geolocate_ip ) {
			$current_user_country = $geolocate_ip['country'];
			if ( $current_user_country && '-' !== $current_user_country ) {
				$country_img_url = \WPSecurityNinja\Plugin\Utils::get_country_img__premium_only( $current_user_country );

				if ( ! isset( $geoip_countrylist ) ) {
					include WF_SN_PLUGIN_DIR . 'modules/cloud-firewall/class-sn-geoip-countrylist.php';
				}
				$country_name = '';
				if ( isset( $geoip_countrylist[ $current_user_country ] ) ) {
					$country_name = $geoip_countrylist[ $current_user_country ];
				}
				if ( $country_img_url ) {
					$ipcol_output = '<img src="' . esc_url( $country_img_url ) . '" width="20" height="20" class="countryimg" title="' . esc_html( $country_name ) . '"> ' . $visitor->ip;
				}
			}
		}

		if ( ! $visitor ) {
			return false;
		}
		$rowatts = '';

		$rowclasses = '';

		if ( $args['marknew'] ) {
			$rowclasses .= ' newrow';
		}
		$rowclasses .= ' visit-' . intval( $visitor->id );

		$notes = '';
		if ( $visitor->banned ) {
			$rowclasses .= ' blocked';
			$notes      .= __( 'Blocked', 'security-ninja' );
			if ( $visitor->ban_reason ) {
				$notes .= ': ' . esc_html( $visitor->ban_reason );
			}
		}

		if ( Wf_sn_cf::is_banned_ip( $visitor->ip ) ) {
			$rowclasses .= ' blocked';
			$notes      .= __( 'Blacklisted by IP', 'security-ninja' );
			if ( $visitor->ban_reason ) {
				$notes .= ': ' . esc_html( $visitor->ban_reason );
			}
		}

		$rowatts = ' class="' . $rowclasses . '"';

		$details  = '<details><summary>' . __( 'Details', 'security-ninja' ) . '</summary><dl>';
		$details .= '<dt>' . __( 'User Agent', 'security-ninja' ) . '</dt><dd>' . esc_html( $visitor->user_agent ) . '</dd>';

		if ( $visitor->action ) {
			$details .= '<dt>' . __( 'Action', 'security-ninja' ) . '</dt><dd>' . esc_html( $visitor->action ) . '</dd>';
		}

		if ( maybe_unserialize( $visitor->description ) ) {
			$details .= '<dt>' . __( 'Description', 'security-ninja' ) . '</dt><dd>' . esc_html( wp_json_encode( $visitor->description ) ) . '</dd>';
		}

		if ( ! empty( $visitor->raw_data ) ) {
			$parsed_raw_data = json_decode( $visitor->raw_data );
		} else {
			$parsed_raw_data = false;
		}

		if ( ( is_array( $parsed_raw_data ) || ( is_object( $parsed_raw_data ) ) ) && ! empty( $parsed_raw_data ) ) {
			$details .= '<dt>' . __( 'Details', 'security-ninja' ) . '</dt><dd>';
			$details .= '<dl class="inner">';
			foreach ( $parsed_raw_data as $key => $rd ) {
				$details .= '<dt>' . esc_html( $key ) . '</dt><dd>' . esc_html( $rd ) . '</dd>';
			}
			$details .= '</dl>';
			$details .= '</dd>';
		} elseif ( is_string( $parsed_raw_data ) ) {
			$details .= '<dt>' . __( 'Details', 'security-ninja' ) . '</dt><dd>' . esc_html( $parsed_raw_data ) . '</dd>';
		}

		$details .= '</dl></details>';

		$output  = '<tr ' . $rowatts . ' >';
		$output .= '<td>' . esc_html( $visitor->timestamp ) . '</td>';
		$output .= '<td>' . wp_kses_post( $ipcol_output ) . '</td>';
		$output .= '<td>' . esc_url( $visitor->URL ) . '</td>';
		$output .= '<td class="secnin-details"><dd>' . wp_kses( $details, $allowed_html ) . '</dd></td>';

		if ( $visitor->banned ) {
			$output .= '<td></td>';
		} elseif ( $args['current_user_ip'] === $visitor->ip ) {
			$output .= '<td class="isme">' . __( 'You', 'security-ninja' ) . '</td>';
		} else {
			$output .= '<td class="secnin-visit-actions">';
			$output .= '<a href="#" data-banip="' . esc_attr( $visitor->ip ) . '" data-nonce="' . esc_attr( $secnin_visitor_log_nonce ) . '" class="button button-small button-secondary secnin-banip">' . __( 'Ban IP', 'security-ninja' ) . '</a>';

			$output .= '</td>';
		}

		$output .= '</tr>';

		return $output;
	}

	/**
	 * Enqueue CSS and JS scripts on plugin's admin page
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.0    Tuesday, May 18th, 2021.
	 * @access  public static
	 * @return  void
	 */
	public static function enqueue_scripts() {
		$current_screen = get_current_screen();
		
		if ( ! $current_screen || strpos( $current_screen->id, 'page_wf-sn-visitor-log' ) === false ) {
			return;
		}

		// Include DataTables from events logger module
		wp_enqueue_style(
			'datatables',
			WF_SN_PLUGIN_URL . 'modules/events-logger/css/jquery.dataTables.min.css',
			array(),
			Wf_Sn::$version
		);

		wp_enqueue_style(
			'datatablesjqueryui',
			WF_SN_PLUGIN_URL . 'modules/events-logger/css/dataTables.jqueryui.min.css',
			array(),
			Wf_Sn::$version
		);

		wp_enqueue_script(
			'datatables',
			WF_SN_PLUGIN_URL . 'modules/events-logger/js/jquery.dataTables.min.js',
			array( 'jquery' ),
			Wf_Sn::$version,
			true
		);

		wp_enqueue_script(
			'secnin-vl',
			WF_SN_PLUGIN_URL . 'modules/visitor-log/js/secnin-visitor-log.js',
			array( 'jquery', 'datatables' ),
			Wf_Sn::$version,
			true
		);

		wp_localize_script(
			'secnin-vl',
			'secnin_vl',
			array(
				'vl_nonce' => wp_create_nonce( 'secnin_visitor_log' ),
				'ajaxurl'  => admin_url( 'admin-ajax.php' ),
				'text'     => array(
					'areyousureblockip' => __( 'Are you sure you want to ban this IP?', 'security-ninja' ),
					'event' => __( 'Event', 'security-ninja' ),
					'url' => __( 'URL', 'security-ninja' ),
					'details' => __( 'Details', 'security-ninja' ),
					'action' => __( 'Action', 'security-ninja' ),
					'errorloadingdata' => __( 'Error loading data:', 'security-ninja' ),
					'status' => __( 'Status:', 'security-ninja' ),
					'error' => __( 'Error:', 'security-ninja' ),
					'code' => __( 'Code:', 'security-ninja' ),
					'response' => __( 'Response:', 'security-ninja' ),
				),
			)
		);
	}

	/**
	 * Return log lines.
	 *
	 * @param int|false $latestid The latest ID to fetch logs from.
	 * @return array Array of log entries.
	 */
	public static function return_log_lines( $latestid = false ) {
		global $wpdb;

		$internal_ignore_list = self::return_filterable_urls();

		$placeholders  = array_fill( 0, count( $internal_ignore_list ), '%s' );
		$like_patterns = array_map(
			function ( $url ) use ( $wpdb ) {
				return '%' . $wpdb->esc_like( $url ) . '%';
			},
			$internal_ignore_list
		);

		$wherestring = $wpdb->prepare(
			'WHERE URL NOT LIKE ' . implode( ' AND URL NOT LIKE ', $placeholders ),
			$like_patterns
		);

		if ( $latestid ) {
			$wherestring .= $wpdb->prepare( ' AND id > %d', $latestid );
		}

		$log_table    = $wpdb->prefix . 'wf_sn_cf_vl';
		$query_string = "SELECT * FROM {$log_table} {$wherestring} ORDER by id DESC LIMIT 100";
		$results      = $wpdb->get_results( $query_string, OBJECT );

		if ( $wpdb->last_error ) {
			return array(); // Return an empty array instead of potentially invalid results
		}

		return $results;
	}

	/**
	 * Return the latest log ID.
	 *
	 * @param int $offset Offset for the query.
	 * @return int|string The highest ID or 0 if no results.
	 */
	public static function return_latest_log_id( $offset = 0 ) {
		global $wpdb;

		$internal_ignore_list = self::return_filterable_urls();
		$placeholders  = array_fill( 0, count( $internal_ignore_list ), '%s' );
		$like_patterns = array_map(
			function ( $url ) use ( $wpdb ) {
				return '%' . $wpdb->esc_like( $url ) . '%';
			},
			$internal_ignore_list
		);

		$wherestring = $wpdb->prepare(
			'WHERE URL NOT LIKE ' . implode( ' AND URL NOT LIKE ', $placeholders ),
			$like_patterns
		);

		$log_table    = $wpdb->prefix . 'wf_sn_cf_vl';
		$query_string = $wpdb->prepare(
			"SELECT id FROM {$log_table} {$wherestring} ORDER by id DESC LIMIT 1 OFFSET %d",
			intval( $offset )
		);
		$highestid    = $wpdb->get_var( $query_string );

		if ( ! $highestid ) {
			$highestid = 0;
		}
		return $highestid;
	}

	/**
	 * AJAX handler for returning latest visitors.
	 *
	 * @return void
	 */
	public static function do_ajax_return_latest_visitors() {

		if ( ! check_ajax_referer( 'secnin_visitor_log', false, false ) ) {
			wp_send_json_error( __( 'Invalid nonce', 'security-ninja' ) );
		}

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error(
				array(
					'message' => __( 'Failed.', 'security-ninja' ),
				)
			);
		}

		$latestid = isset( $_GET['current'] ) ? sanitize_text_field( $_GET['current'] ) : '';

		$results = self::return_log_lines( $latestid );

		if ( $results ) {
			$highestid       = false;
			$latestvisits    = array();
			$current_user_ip = \WPSecurityNinja\Plugin\Wf_sn_cf::get_user_ip();
			$fmtlineargs     = array(
				'current_user_ip' => $current_user_ip,
			);
			foreach ( $results as $re ) {
				$highestid      = $re->id;
				$latestvisits[] = self::format_line( $re, $fmtlineargs );
			}
			$latestvisits = array_reverse( $latestvisits );
		}

		if ( $results ) {
			$output = array(
				'visits'  => $latestvisits,
				'current' => $highestid,
			);
			wp_send_json_success( $output );
		} else {
			wp_send_json_error( __( 'No data', 'security-ninja' ) );
		}
	}

	/**
	 * Ban IP via AJAX.
	 *
	 * @return void
	 */
	public static function do_ajax_banip() {

		if ( ! check_ajax_referer( 'secnin_visitor_log', false, false ) ) {
			wp_send_json_error( __( 'Invalid nonce', 'security-ninja' ) );
		}

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error(
				array(
					'message' => __( 'Failed.', 'security-ninja' ),
				)
			);
		}

		$banip = isset( $_POST['banip'] ) ? sanitize_text_field( $_POST['banip'] ) : false;

		if ( ! $banip || ! filter_var( $banip, FILTER_VALIDATE_IP ) ) {
			wp_send_json_error( __( 'Please enter a valid IP.', 'security-ninja' ) );
		}

		// Add this check to prevent banning localhost or private IPs
		if ( filter_var( $banip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) === false ) {
			wp_send_json_error( __( 'Cannot ban private or reserved IP addresses.', 'security-ninja' ) );
		}

		if ( ! defined( 'WF_SN_CF_OPTIONS_KEY' ) ) {
			return false;
		}

		$cf_options              = get_option( WF_SN_CF_OPTIONS_KEY );
		$blacklist               = $cf_options['blacklist'];
		$blacklist[]             = $banip;
		$blacklist               = array_unique( $blacklist );
		$cf_options['blacklist'] = $blacklist;
		update_option( WF_SN_CF_OPTIONS_KEY, $cf_options, false );
		wp_send_json_success( __( 'Added IP to blacklist', 'security-ninja' ) );
		wf_sn_el_modules::log_event( 'security_ninja', 'blacklisted_ip', __( 'New IP added to the blacklist manually from the visitor log.', 'security-ninja' ), '' );
		return false;
	}

	/**
	 * Display the visitor log page
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.2    Wednesday, January 11th, 2023.
	 * @access  public static
	 * @return  void
	 */
	public static function live_log_page() {
		$options     = Wf_sn_cf::get_options();
		$trackvisits = intval( $options['trackvisits'] );

		if ( ! $trackvisits ) {
			?>
<div class="wrap">  
			<?php \WPSecurityNinja\Plugin\Utils::show_topbar(); ?>
			<div class="secnin-live-vis sncard">
				<h2><?php esc_html_e( 'Visitor log is not enabled!', 'security-ninja' ); ?></h2>
				
				<ol>
					<li><p>
					<?php
					printf(
						/* translators: %s: URL to firewall settings page */
						esc_html__( 'Go to the %s.', 'security-ninja' ),
						sprintf(
							'<a href="%s">%s</a>',
							esc_url( admin_url( 'admin.php?page=wf-sn#sn_cf' ) ),
							esc_html__( 'Firewall settings', 'security-ninja' )
						)
					);
					?>
							</p></li>
					<li><p><?php esc_html_e( 'Scroll down and select the "Visitor Logging" tab to enable the feature.', 'security-ninja' ); ?></p></li>
				</ol>
			</div>
		</div>
		
			<?php
			return;
		}
		?>
		<div class="wrap">

			<?php \WPSecurityNinja\Plugin\Utils::show_topbar(); ?>
			<div class="sncard">
			<div class="tablenav top">
				<div class="alignleft actions">
					<h2><?php esc_html_e( 'Visitor log', 'security-ninja' ); ?></h2>
				</div>
				<div class="alignright actions">
					<button type="button" id="refresh-visitor-log" class="button">
						<span class="dashicons dashicons-update" style="vertical-align: middle;"></span>
						<?php esc_html_e( 'Refresh', 'security-ninja' ); ?>
					</button>
				</div>
			</div>
			</div>
			<div class="secnin-live-vis sncard">
			<table class="wp-list-table widefat fixed table-view-list" id="secnin-visitor-log" style="border-spacing: 0;">
				<thead>
					<tr>
						<th class="column-primary"><?php esc_html_e( 'Event', 'security-ninja' ); ?></th>
						<th><?php esc_html_e( 'URL', 'security-ninja' ); ?></th>
						<th><?php esc_html_e( 'Details', 'security-ninja' ); ?></th>
						<th><?php esc_html_e( 'Action', 'security-ninja' ); ?></th>
					</tr>
				</thead>
				<tbody>
				</tbody>
				<tfoot>
					<tr>
						<th class="column-primary"><?php esc_html_e( 'Event', 'security-ninja' ); ?></th>
						<th><?php esc_html_e( 'URL', 'security-ninja' ); ?></th>
						<th><?php esc_html_e( 'Details', 'security-ninja' ); ?></th>
						<th><?php esc_html_e( 'Action', 'security-ninja' ); ?></th>
					</tr>
				</tfoot>
			</table>

			<?php
$return_filterable_urls = Wf_Sn_Visitor_log::return_filterable_urls();
if ($return_filterable_urls) {
	?>
<small><?php esc_html_e( 'Filtering out:', 'security-ninja' ); ?>
	<?php
echo  implode( ', ', $return_filterable_urls );
}
			?></small>
			<div id="datatable-error" class="sncard" style="display:none;"></div>
			</div>
		</div>
		<?php
	}

	/**
	 * ajax_get_visitor_data.
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.0  Wednesday, June 4th, 2025.
	 * @access  public static
	 * @return  void
	 */
	public static function ajax_get_visitor_data() {
		check_ajax_referer( 'secnin_visitor_log', 'nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( 'Unauthorized access' );
			exit();
		}

		global $wpdb;
		$log_table = $wpdb->prefix . 'wf_sn_cf_vl';

		// DataTables server-side parameters
		$draw   = isset( $_POST['draw'] ) ? intval( $_POST['draw'] ) : 1;
		$start  = isset( $_POST['start'] ) ? intval( $_POST['start'] ) : 0;
		$length = isset( $_POST['length'] ) ? intval( $_POST['length'] ) : 10;
		$search = isset( $_POST['search']['value'] ) ? sanitize_text_field( $_POST['search']['value'] ) : '';

		// Build query
		$where = array( '1=1' );
		if ( $search ) {
			$where[] = $wpdb->prepare(
				'(ip LIKE %s OR URL LIKE %s OR user_agent LIKE %s)',
				'%' . $wpdb->esc_like( $search ) . '%',
				'%' . $wpdb->esc_like( $search ) . '%',
				'%' . $wpdb->esc_like( $search ) . '%'
			);
		}

		// Add filterable URLs exclusion
		$filterable_urls = self::return_filterable_urls();
		if ( ! empty( $filterable_urls ) ) {
			$url_conditions = array();
			foreach ( $filterable_urls as $url ) {
				$url_conditions[] = $wpdb->prepare( 'URL NOT LIKE %s', '%' . $wpdb->esc_like( $url ) . '%' );
			}
			if ( ! empty( $url_conditions ) ) {
				$where[] = '(' . implode( ' AND ', $url_conditions ) . ')';
			}
		}

		$where_clause = implode( ' AND ', $where );

		// Get total and filtered counts
		$total_query    = "SELECT COUNT(*) FROM {$log_table}";
		$filtered_query = "SELECT COUNT(*) FROM {$log_table} WHERE {$where_clause}";
		// Get data
		$data_query = $wpdb->prepare(
			"SELECT * FROM {$log_table} 
            WHERE {$where_clause} 
            ORDER BY timestamp DESC 
            LIMIT %d OFFSET %d",
			$length,
			$start
		);

		$total    = $wpdb->get_var( $total_query );
		$filtered = $wpdb->get_var( $filtered_query );
		$results  = $wpdb->get_results( $data_query );

		$data = array();
		foreach ( $results as $row ) {
			$data[] = self::format_datatable_row( $row );
		}

		wp_send_json(
			array(
				'draw'            => $draw,
				'recordsTotal'    => $total,
				'recordsFiltered' => $filtered,
				'data'            => $data,
			)
		);
	}

	/**
	 * format_datatable_row.
	 *
	 * @author  Lars Koudal
	 * @since   v0.0.1
	 * @version v1.0.0  Wednesday, June 4th, 2025.
	 * @access  private static
	 * @param   mixed   $row
	 * @return  mixed
	 */
	private static function format_datatable_row( $row ) {
		$current_user_ip = \WPSecurityNinja\Plugin\Wf_sn_cf::get_user_ip();

		// Format timestamp
		$timestamp = mysql2date( 'Y-m-d H:i:s', $row->timestamp );

		// Format IP with country if available
		$country = ! empty( $row->country ) ? ' (' . esc_html( $row->country ) . ')' : '';

		$country_img_url = \WPSecurityNinja\Plugin\Utils::get_country_img__premium_only( $row->country );

		if ( $country_img_url ) {
			$country_img_url = '<img src="' . esc_url( $country_img_url ) . '" width="20" height="20" class="countryimg" title="' . esc_html( $row->country ) . '">';
		} else {
			$country_img_url = '';
		}
		$ip_display = esc_html( $row->ip ) . $country_img_url;

		// Format URL
		$url_display = '<a href="' . esc_url( $row->URL ) . '" target="_blank">' . esc_html( $row->URL ) . '</a>';

		// Format details - removed platform since it doesn't exist

		/* translators: %s: Browser user agent */
		$details = sprintf(
			__( 'Browser: %s', 'security-ninja' ),
			esc_html( $row->user_agent )
		);

		// Format actions
		$actions = '';
		if ( $row->ip !== $current_user_ip ) {
			$actions = sprintf(
				'<a href="#" class="secnin-banip" data-nonce="%s" data-banip="%s">%s</a>',
				wp_create_nonce( 'secnin_visitor_log' ),
				esc_attr( $row->ip ),
				__( 'Ban IP', 'security-ninja' )
			);
		}

		return array(
			'timestamp' => $timestamp . '<br>' . $ip_display,
			'url'       => $url_display,
			'details'   => $details,
			'actions'   => $actions,
		);
	}
}

add_action( 'plugins_loaded', array( __NAMESPACE__ . '\Wf_Sn_Visitor_log', 'init' ) );
