<?php
/**
 * Access class
 *
 * @package ZeroSpam
 */

namespace ZeroSpam\Core;

use ZeroSpam;

// Security Note: Blocks direct access to the plugin PHP files.
defined( 'ABSPATH' ) || die();

/**
 * Access
 */
class Access {
	/**
	 * Constructor
	 */
	public function __construct() {
		add_action( 'init', array( $this, 'init' ) );
	}

	/**
	 * Initializes the class by setting up hooks and actions.
	 *
	 * This method is called during the WordPress initialization process. It
	 * registers the 'template_redirect' action to perform access checks and
	 * adds a filter for 'zerospam_access_checks' to determine if the current
	 * request should be blocked.
	 */
	public function init() {
		if ( ! is_admin() && self::process() ) {
			add_action( 'template_redirect', array( $this, 'access_check' ), 0 );
			add_filter( 'zerospam_access_checks', array( $this, 'check_blocked' ), 0, 3 );
		}

		// Protect login.
		add_action( 'login_init', array( $this, 'login_access_check' ) );

		// Protect XML-RPC.
		add_action( 'xmlrpc_call', array( $this, 'xmlrpc_access_check' ) );
	}

	/**
	 * Terminates execution with a custom error message and HTTP status code.
	 *
	 * Registers an action to prevent caching on error conditions by setting
	 * appropriate HTTP headers before leveraging WordPress's wp_die() function
	 * to produce an error page with a specified message, title, and HTTP status
	 * code.
	 *
	 * @param string $title   The text to be used as the page title for the error message.
	 *                        This content will be sanitized to remove unwanted HTML.
	 * @param string $message The error message to display. This content will be escaped
	 *                        to ensure only safe HTML is included.
	 * @param int    $code    Optional. The HTTP status code to be sent in the header.
	 *                        Defaults to 403 to indicate a Forbidden error.
	 */
	public static function terminate_execution( $title, $message, $code = 403 ) {
		if ( ! defined( 'DONOTCACHEPAGE' ) ) {
			define( 'DONOTCACHEPAGE', true );
		}
		nocache_headers();

		wp_die(
			wp_kses_post( $message ),
			esc_html( $title ),
			[
				'response' => esc_html( $code ),
			]
		);
	}

	/**
	 * Determines is security checks need to be triggers.
	 *
	 * @param boolean $ignore_ajax True if AJAX shouldn't be checked.
	 */
	public static function process( $ignore_ajax = false ) {
		if ( empty( $_SERVER['REQUEST_URI'] ) ) {
			return false;
		}

		$user_ip = \ZeroSpam\Core\User::get_ip();

		// Check for rescue mode.
		if ( defined( 'ZEROSPAM_RESCUE_KEY' ) && ! empty( $_GET['zerospam_rescue'] ) ) {
			if ( hash_equals( ZEROSPAM_RESCUE_KEY, $_GET['zerospam_rescue'] ) ) {
				return false;
			}
		}

		// Sanitize the REQUEST_URI before further processing.
		$request_uri = esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) );

		// Check for .ico requests.
		$path = wp_parse_url( $request_uri, PHP_URL_PATH );
		if ( substr( $path, -4 ) === '.ico' ) {
			return false;
		}

		if ( ( $ignore_ajax && is_admin() ) || is_user_logged_in() || \ZeroSpam\Core\Utilities::is_whitelisted( $user_ip ) ) {
			return false;
		} elseif ( ! $ignore_ajax && ( ( is_admin() && ! wp_doing_ajax() ) || is_user_logged_in() ) ) {
			return false;
		}

		return true;
	}

	/**
	 * Access check
	 */
	public function access_check() {
		// Ensure this is the main query for standard template redirects.
		if ( ! is_main_query() ) {
			return;
		}

		$this->run_block_check();
	}

	/**
	 * Login Access check
	 */
	public function login_access_check() {
		if ( self::process( true ) ) {
			$this->run_block_check();
		}
	}

	/**
	 * XML-RPC Access check
	 */
	public function xmlrpc_access_check() {
		if ( self::process( true ) ) {
			$this->run_block_check( true );
		}
	}

	/**
	 * Runs the block check logic.
	 *
	 * @param boolean $is_xmlrpc True if this is an XML-RPC request.
	 */
	public function run_block_check( $is_xmlrpc = false ) {
		$access = self::get_access();

		if ( ! empty( $access['blocked'] ) ) {
			$settings = ZeroSpam\Core\Settings::get_settings();

			if ( ! empty( $access['details'] ) && is_array( $access['details'] ) ) {
				foreach ( $access['details'] as $key => $detail ) {
					if ( ! empty( $detail['blocked'] ) ) {
						if ( empty( $detail['details']['failed'] ) ) {
							$detail['details']['failed'] = $key;
						}

						if ( ! empty( $settings['log_blocked_ips']['value'] ) && 'enabled' === $settings['log_blocked_ips']['value'] ) {
							ZeroSpam\Includes\DB::log( $detail['type'], $detail['details'] );
						}
					}
				}
			}

			if ( $is_xmlrpc ) {
				wp_die(
					'<?xml version="1.0"?><methodResponse><fault><value><struct><member><name>faultCode</name><value><int>403</int></value></member><member><name>faultString</name><value><string>' . esc_html__( 'Access Denied: Your IP address has been blocked due to suspected malicious activity.', 'zero-spam' ) . '</string></value></member></struct></value></fault></methodResponse>',
					'XML-RPC Access Denied',
					array( 'response' => 403, 'Content-Type' => 'text/xml' )
				);
			}

			if ( ! empty( $settings['block_handler']['value'] ) ) {
				switch ( $settings['block_handler']['value'] ) {
					case 403:
						$message = __( 'Your IP address has been blocked due to detected spam/malicious activity.', 'zero-spam' );
						if ( ! empty( $settings['blocked_message']['value'] ) ) {
							$message = $settings['blocked_message']['value'];
						}

						self::terminate_execution( __( 'Blocked', 'zero-spam' ), $message );
						break;
					case 'redirect':
						$url = 'https://wordpress.org/plugins/zero-spam/';
						if ( ! empty( $settings['blocked_redirect_url']['value'] ) ) {
							$url = esc_url( $settings['blocked_redirect_url']['value'] );
						}
						wp_redirect( $url );
						exit;
						break;
				}
			}
		}
	}

	/**
	 * Helper to get blocked record details.
	 */
	public static function get_blocked_details( $blocked_record, $failed = false ) {
		$access_check = array(
			'blocked' => false,
		);

		if ( ! $blocked_record ) {
			return $access_check;
		}

		$today = new \DateTime();

		// Check the start & end dates (all blocks require a start date).
		$start_date = new \DateTime();
		if ( ! empty( $blocked_record['start_block'] ) ) {
			$start_date = new \DateTime( $blocked_record['start_block'] );
		}

		$blocked = false;
		if ( $today >= $start_date ) {
			// Check the end date if temporary block.
			if (
				! empty( $blocked_record['blocked_type'] ) &&
				'temporary' === $blocked_record['blocked_type']
			) {
				// Temporary block.
				if ( ! empty( $blocked_record['end_block'] ) ) {
					$end_date = new \DateTime( $blocked_record['end_block'] );

					if ( $today < $end_date ) {
						$blocked = true;
					}
				}
			} else {
				// Permanent block.
				$blocked = true;
			}
		}

		if ( $blocked ) {
			$access_check['blocked']           = true;
			$access_check['type']              = 'blocked';
			$access_check['details']           = $blocked_record;
			$access_check['details']['failed'] = $failed;
		}

		return $access_check;
	}

	/**
	 * Checks if an IP has been blocked
	 *
	 * @param array  $access_checks Array of existing access checks.
	 * @param string $user_ip The user's IP address.
	 * @param array  $settings The plugin settings.
	 */
	public function check_blocked( $access_checks, $user_ip, $settings ) {
		$access_checks['blocked'] = false;

		// Check if geolocation information is available, if so, check if blocked.
		$geolocation_information = \ZeroSpam\Core\Utilities::geolocation( $user_ip );
		if ( $geolocation_information ) {
			// Geolocation information available, check the blocked locations.
			// Available blocked location keys.
			$location_keys = array(
				'country_code',
				'region_code',
				'city',
				'zip',
			);

			foreach ( $location_keys as $key => $loc ) {
				if ( ! empty( $geolocation_information[ $loc ] ) ) {
					$blocked = \ZeroSpam\Includes\DB::blocked( $geolocation_information[ $loc ], $loc );
					if ( $blocked ) {
						$access_checks['blocked'] = self::get_blocked_details( $blocked, 'blocked_' . $loc );
						break;
					}
				}
			}
		}

		// If passed location blocks, check the IP address.
		if ( ! $access_checks['blocked'] ) {
			// Check the user's IP access.
			$blocked = \ZeroSpam\Includes\DB::blocked( $user_ip );
			if ( $blocked ) {
				$access_checks['blocked'] = self::get_blocked_details( $blocked, 'blocked_ip' );
			}
		}

		return $access_checks;
	}

	/**
	 * Gets the current user's access
	 */
	public function get_access() {
		$settings = ZeroSpam\Core\Settings::get_settings();
		$user_ip  = ZeroSpam\Core\User::get_ip();

		$access = array(
			'blocked' => false,
		);

		if ( $user_ip ) {
			$access['ip'] = $user_ip;

			if ( ZeroSpam\Core\Utilities::is_whitelisted( $user_ip ) ) {
				return $access;
			}

			$access_checks = apply_filters(
				'zerospam_access_checks',
				array(),
				$user_ip,
				$settings
			);

			foreach ( $access_checks as $key => $check ) {
				if ( ! empty( $check['blocked'] ) ) {
					$access['blocked'] = true;
					break;
				}
			}

			$access['details'] = $access_checks;
		}

		return $access;
	}
}
