<?php

if (!defined('ABSPATH')) die('Access denied.');

if (!class_exists('AIOWPSecurity_Ajax')) :

	class AIOWPSecurity_Ajax {

		private $commands_object;

		private $nonce;

		private $subaction;

		private $data;

		private $results;

		/**
		 * Seconds for AIOS heartbeat interval
		 *
		 * @var int
		 */
		const HEARTBEAT_INTERVAL = 10; // in seconds

		/**
		 * Constructor
		 */
		private function __construct() {
			if (!class_exists('AIOWPSecurity_Commands')) include_once(AIO_WP_SECURITY_PATH.'/classes/wp-security-commands.php');
			$this->commands_object = new AIOWPSecurity_Commands();

			add_action('wp_ajax_aios_ajax', array($this, 'handle_ajax_requests'));
			add_action('wp_ajax_nopriv_get_antibot_keys', array($this->commands_object, 'get_antibot_keys'));
		}

		/**
		 * Return singleton instance
		 *
		 * @return AIOWPSecurity_Ajax Returns AIOWPSecurity_Ajax object
		 */
		public static function get_instance() {
			static $instance = null;
			if (null === $instance) {
				$instance = new self();
			}
			return $instance;
		}

		/**
		 * Handles ajax requests
		 *
		 * @return void
		 */
		public function handle_ajax_requests() {
			$this->set_nonce();
			$this->set_subaction();
			$this->set_data();

			$result = AIOWPSecurity_Utility_Permissions::check_nonce_and_user_cap($this->nonce, 'wp-security-ajax-nonce');
			if (is_wp_error($result)) {
				wp_send_json(array(
					'result' => false,
					'error_code' => $result->get_error_code(),
					'error_message' => $result->get_error_message(),
				));
			}

			if (is_multisite() && !current_user_can('manage_network_options')) {
				if (!$this->is_valid_multisite_command()) {
					$this->send_invalid_multisite_command_error_response();
				}
			}

			if ($this->is_invalid_command()) {
				$this->add_invalid_command_error_log_entry();
				$this->set_invalid_command_error_response();
			} else {
				$this->execute_command();
				$this->set_error_response_on_wp_error();
				$this->maybe_set_results_as_null();
			}

			$this->json_encode_results();

			$json_last_error = json_last_error();
			if ($json_last_error) {
				$this->set_error_response_on_json_encode_error($json_last_error);
			}

			echo $this->results; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Variable is an array containing escaped data.
			die;
		}

		/**
		 * Sets nonce property value
		 *
		 * @return void
		 */
		private function set_nonce() {
			// phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput -- It's the actual nonce. It does not need sanitizing.
			$this->nonce = empty($_POST['nonce']) ? '' : $_POST['nonce'];
		}

		/**
		 * Sets subaction property value
		 *
		 * @return void
		 */
		private function set_subaction() {
			// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce already checked.
			$this->subaction = empty($_POST['subaction']) ? '' : sanitize_text_field(wp_unslash($_POST['subaction']));
		}

		/**
		 * Sets data property value
		 *
		 * @return void
		 */
		private function set_data() {
			// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce already checked.
			$this->data = isset($_POST['data']) ? wp_unslash($_POST['data']) : null;
		}

		/**
		 * Checks whether it is multisite setup and command is valid multisite command
		 *
		 * @return bool
		 */
		private function is_valid_multisite_command() {
			/**
			 * Filters the commands allowed to the sub site admins. Other commands are only available to network admin. Only used in a multisite context.
			 */
			$allowed_commands = apply_filters('aios_multisite_allowed_commands', array(
				'delete_audit_log',
				'render_audit_log_tab',
				'delete_404_log',
				'perform_delete_all_404_event_records',
				'render_404_log_tab',
				'delete_locked_ip_record',
				'clear_debug_logs',
				'unlock_ip',
				'blocked_ip_list_unblock_ip',
				'lock_ip',
				'dismiss_notice',
			));

			return in_array($this->subaction, $allowed_commands);
		}

		/**
		 * Sends the invalid multisite command error response
		 *
		 * @return void
		 */
		private function send_invalid_multisite_command_error_response() {
			wp_send_json(array(
				'result' => false,
				'error_code' => 'update_failed',
				'error_message' => __('Options can only be saved by network admin', 'all-in-one-wp-security-and-firewall')
			));
		}

		/**
		 * Checks if applied ajax command is an invalid command or not
		 *
		 * @return bool Returns true if ajax command is an invalid command, false otherwise
		 */
		private function is_invalid_command() {
			return !is_callable(array($this->commands_object, $this->subaction));
		}

		/**
		 * Log an error message for invalid ajax command
		 *
		 * @return void
		 */
		private function add_invalid_command_error_log_entry() {
			// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Part of error reporting.
			error_log("AIOS: ajax_handler: no such command (" . $this->subaction . ")");
		}

		/**
		 * Set `results` property with error response array for invalid ajax command
		 *
		 * @return void
		 */
		private function set_invalid_command_error_response() {
			$this->results = array(
				'result' => false,
				'error_code' => 'command_not_found',
				/* translators: %s: Subaction */
				'error_message' => sprintf(__('The command "%s" was not found', 'all-in-one-wp-security-and-firewall'), $this->subaction)
			);
		}

		/**
		 * Execute the ajax command
		 *
		 * @return void
		 */
		private function execute_command() {
			$this->results = call_user_func(array($this->commands_object, $this->subaction), $this->data);
		}

		/**
		 * Set `results` property with error message
		 *
		 * @return void
		 */
		private function set_error_response_on_wp_error() {
			if (is_wp_error($this->results)) {
				$this->results = array(
					'result' => false,
					'error_code' => $this->results->get_error_code(),
					'error_message' => $this->results->get_error_message(),
					'error_data' => $this->results->get_error_data(),
				);
			}
		}

		/**
		 * Set `results` property to null, if it is not yet set
		 *
		 * @return void
		 */
		private function maybe_set_results_as_null() {
			// if nothing was returned for some reason, set as result null.
			if (empty($this->results)) {
				$this->results = array(
					'result' => null
				);
			}
		}

		/**
		 * Sets `results` property with json encode error
		 *
		 * @param int $json_last_error
		 *
		 * @return void
		 */
		private function set_error_response_on_json_encode_error($json_last_error) {
			$this->results = array(
				'result' => false,
				'error_code' => $json_last_error,
				'error_message' => 'json_encode error : ' . $json_last_error,
				'error_data' => '',
			);

			$this->results = wp_json_encode($this->results);
		}

		/**
		 * Json encode the `results` property value
		 *
		 * @return void
		 */
		private function json_encode_results() {
			$this->results = wp_json_encode($this->results);
		}
	}

endif;
