<?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 );

				$geoip_countrylist = \WPSecurityNinja\Plugin\Utils::get_country_list();
				$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>';

		$allowed_html = array(
			'details' => array(),
			'summary' => array(),
			'dl'      => array( 'class' => array() ),
			'dt'      => array(),
			'dd'      => array(),
		);

		$output  = '<tr ' . $rowatts . ' >';
		$output .= '<td>' . esc_html( $visitor->timestamp ) . '</td>';
		$output .= '<td>' . wp_kses_post( $ipcol_output ) . '</td>';
		$output .= '<td>' . esc_html( $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">
						<?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 = self::return_filterable_urls();
						if ( $return_filterable_urls ) {
							?>
							<small><?php esc_html_e( 'Filtering out:', 'security-ninja' ); ?>
							<?php
							echo implode( ', ', array_map( 'esc_html', $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() {
		if ( ! check_ajax_referer( 'secnin_visitor_log', 'nonce', false ) ) {
			wp_send_json_error( __( 'Invalid nonce', 'security-ninja' ) );
		}

		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 );

		// Resolve country: prefer stored value, fallback to geolocation (for older entries).
		$country_code = ! empty( $row->country ) && '-' !== $row->country ? $row->country : '';
		if ( empty( $country_code ) ) {
			if ( ! class_exists( __NAMESPACE__ . '\\SN_Geolocation' ) ) {
				include_once WF_SN_PLUGIN_DIR . 'modules/cloud-firewall/class-sn-geolocation.php';
			}
			if ( class_exists( __NAMESPACE__ . '\\SN_Geolocation' ) ) {
				$geolocate_ip = \WPSecurityNinja\Plugin\SN_Geolocation::geolocate_ip( $row->ip, true );
				if ( ! empty( $geolocate_ip['country'] ) && '-' !== $geolocate_ip['country'] ) {
					$country_code = $geolocate_ip['country'];
				}
			}
		}

		$country_img_html = '';
		if ( $country_code ) {
			$country_img_url = \WPSecurityNinja\Plugin\Utils::get_country_img__premium_only( $country_code );
			if ( $country_img_url ) {
				$geoip_countrylist = \WPSecurityNinja\Plugin\Utils::get_country_list();
				$country_name      = isset( $geoip_countrylist[ $country_code ] ) ? $geoip_countrylist[ $country_code ] : $country_code;
				$country_img_html = '<img src="' . esc_url( $country_img_url ) . '" width="20" height="20" class="countryimg" title="' . esc_attr( $country_name ) . '"> ';
			}
		}

		// Flag first, then IP (match Event Log).
		$ip_display = $country_img_html . esc_html( $row->ip );

		// 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' ) );

