<?php
/**
 * Class for providing data the WordPress' Site Health debug information.
 *
 * @package JupiterX_Core\Admin
 *
 * @since 1.18.0
 */

/**
 * Site Health class.
 *
 * @since 1.18.0
 *
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
 */
class JupiterX_Core_Site_Health {

	/**
	 * Site health class instance.
	 *
	 * @since 1.18.0
	 *
	 * @var JupiterX_Core_Site_Health|null
	 */
	private static $instance = null;

	/**
	 * Return an instance of the JupiterX_Core_Site_Health class, or create one if none exist yet.
	 *
	 * @since 1.18.0
	 *
	 * @return JupiterX_Core_Site_Health|null
	 */
	public static function get_instance() {
		if ( null === self::$instance ) {
			self::$instance = new JupiterX_Core_Site_Health();
		}

		return self::$instance;
	}

	/**
	 * Construct class.
	 *
	 * @since 1.18.0
	 */
	public function __construct() {
		add_action( 'wp_ajax_jupiterx_system_status', [ $this, 'system_status' ] );
		add_filter( 'debug_information', [ $this, 'debug_information' ] );
		add_filter( 'site_status_tests', [ $this, 'site_status_tests' ] );
	}

	/**
	 * Get site status using WP_Site_Health class.
	 *
	 * @since 1.18.0
	 *
	 * @return void
	 *
	 * @SuppressWarnings(PHPMD.NPathComplexity)
	 */
	public function system_status() {
		$health_check_js_variables = [
			'site_status' => [
				'direct' => [],
				'async'  => [],
				'issues' => [
					'good'        => 0,
					'recommended' => 0,
					'critical'    => 0,
				],
			],
		];

		$issue_counts = get_transient( 'health-check-site-status-result' );

		jupiterx_log(
			'[Control Panel > Dashboard > Site Health] To get site health issues, the following data is expected to be an object.',
			$issue_counts
		);

		if ( false !== $issue_counts ) {
			$issue_counts = json_decode( $issue_counts );

			$health_check_js_variables['site_status']['issues'] = $issue_counts;
		}

		$core_current_version = get_bloginfo( 'version' );

		if ( version_compare( $core_current_version, '5.2', '<' ) ) {
			jupiterx_log(
				'[Control Panel > Dashboard > Site Health] To show site health issues, WordPress needs to be updated.',
				$core_current_version
			);

			$health_check_js_variables['site_status']['direct'][] = [
				'label'       => sprintf(
					// translators: %s Site's current WordPress version
					__( 'WordPress version (%s) is outdated', 'jupiterx-core' ),
					$core_current_version
				),
				'status'      => 'critical',
				'badge'       => [
					'label' => 'Performance',
					'color' => 'blue',
				],
				'actions'     => sprintf(
					'<a href="%s">%s</a>',
					esc_url( admin_url( 'update-core.php' ) ),
					__( 'Update core', 'jupiterx-core' )
				),
				'description' => '',
				'test'        => 'wordpress_version',
			];

			wp_send_json_success( $health_check_js_variables );
		}

		if ( ! class_exists( 'WP_Site_Health' ) ) {
			require_once ABSPATH . 'wp-admin/includes/class-wp-site-health.php';
		}

		$site_health = new WP_Site_Health();

		// Start running tests.
		$tests = $site_health::get_tests();

		jupiterx_log(
			'[Control Panel > Dashboard > Site Health] To show site health issues, the following data is expected to be an array.',
			$tests
		);

		// Don't run https test on localhost.
		if ( 'localhost' === preg_replace( '|https?://|', '', get_site_url() ) ) {
			unset( $tests['direct']['https_status'] );
		}

		foreach ( $tests['direct'] as $test ) {
			if ( is_string( $test['test'] ) ) {
				$test_function = sprintf(
					'get_test_%s',
					$test['test']
				);

				if ( method_exists( $site_health, $test_function ) && is_callable( [ $site_health, $test_function ] ) ) {
					$health_check_js_variables['site_status']['direct'][] = $this->perform_test( [ $site_health, $test_function ] );
					continue;
				}
			}

			if ( is_callable( $test['test'] ) ) {
				$health_check_js_variables['site_status']['direct'][] = $this->perform_test( $test['test'] );
			}
		}

		foreach ( $tests['async'] as $test ) {
			if ( is_string( $test['test'] ) ) {
				$health_check_js_variables['site_status']['async'][] = [
					'test'      => $test['test'],
					'completed' => false,
				];
			}
		}

		wp_send_json_success( $health_check_js_variables );
	}

	/**
	 * Run a Site Health test directly.
	 *
	 * @since 1.18.0
	 *
	 * @param array $callback Callback method for test.
	 *
	 * @return mixed|void
	 */
	private function perform_test( $callback ) {
		// Borrowed filter from WP_Site_Health::perform_test().
		return apply_filters( 'site_status_test_result', call_user_func( $callback ) );
	}

	/**
	 * Add debug data to WP_Debug_Data class.
	 *
	 * @since 1.18.0
	 *
	 * @param array $info WP debug data for the site.
	 *
	 * @return array Filters debug data.
	 *
	 * @SuppressWarnings(PHPMD)
	 */
	public function debug_information( $info ) {
		// Compose common internationalized values.
		$value = [
			'yes'      => __( 'Yes', 'jupiterx-core' ),
			'no'       => __( 'No', 'jupiterx-core' ),
			'enabled'  => __( 'Enabled', 'jupiterx-core' ),
			'disabled' => __( 'Disabled', 'jupiterx-core' ),
		];

		// WordPress section.
		$info['wp-core']['fields']['content_url'] = [
			'label' => __( 'Content URL', 'jupiterx-core' ),
			'value' => WP_CONTENT_URL,
		];

		$upload_dir = wp_get_upload_dir();

		$info['wp-core']['fields']['upload_url'] = [
			'label' => __( 'Upload URL', 'jupiterx-core' ),
			'value' => $upload_dir['baseurl'],
		];
		$info['wp-core']['fields']['front_page'] = [
			'label' => __( 'Front page display', 'jupiterx-core' ),
			'value' => get_option( 'show_on_front' ),
		];

		// Server section.
		// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated
		$is_localhost = ( '127.0.0.1' === $_SERVER['REMOTE_ADDR'] || 'localhost' === $_SERVER['REMOTE_ADDR'] || '::1' === $_SERVER['REMOTE_ADDR'] );

		$info['wp-server']['fields']['localhost'] = [
			'label' => __( 'Is local environment?', 'jupiterx-core' ),
			'value' => $is_localhost ? $value['yes'] : $value['no'],
		];

		$info['wp-server']['fields']['php_errors'] = [
			'label' => __( 'PHP display errors', 'jupiterx-core' ),
			'value' => ini_get( 'display_errors' ) ? $value['yes'] : $value['no'],
		];

		$info['wp-server']['fields']['fsockopen'] = [
			'label' => __( 'Is fsockopen available?', 'jupiterx-core' ),
			'value' => function_exists( 'fsockopen' ) ? $value['yes'] : $value['no'],
		];

		$info['wp-server']['fields']['php_gzopen'] = [
			'label' => __( 'Is gzipopen available?', 'jupiterx-core' ),
			'value' => is_callable( 'gzopen' ) ? $value['yes'] : $value['no'],
		];

		$info['wp-server']['fields']['php_xml'] = [
			'label' => __( 'PHP XML ', 'jupiterx-core' ),
			'value' => function_exists( 'xml_parse' ) ? $value['enabled'] : $value['disabled'],
		];

		$simplexml_loaded = class_exists( 'SimpleXMLElement' ) && function_exists( 'simplexml_load_string' );

		$info['wp-server']['fields']['php_simplexml'] = [
			'label' => __( 'SimpleXML', 'jupiterx-core' ),
			'value' => $simplexml_loaded ? $value['enabled'] : $value['disabled'],
		];

		$mbstring_loaded = extension_loaded( 'mbstring' ) && function_exists( 'mb_eregi' ) && function_exists( 'mb_ereg_match' );

		$info['wp-server']['fields']['php_mbstring'] = [
			'label' => __( 'MBString', 'jupiterx-core' ),
			'value' => $mbstring_loaded ? $value['enabled'] : $value['disabled'],
		];

		$info['wp-server']['fields']['php_soapclient'] = [
			'label' => __( 'SoapClient', 'jupiterx-core' ),
			'value' => class_exists( 'SoapClient' ) ? $value['enabled'] : $value['disabled'],
		];

		$info['wp-server']['fields']['php_domdocument'] = [
			'label' => __( 'DOMDocument', 'jupiterx-core' ),
			'value' => class_exists( 'DOMDocument' ) ? $value['enabled'] : $value['disabled'],
		];

		$info['wp-server']['fields']['php_ziparchive'] = [
			'label' => __( 'ZipArchive', 'jupiterx-core' ),
			'value' => class_exists( 'ZipArchive' ) ? $value['enabled'] : $value['disabled'],
		];

		$info['wp-server']['fields']['php_iconv'] = [
			'label' => __( 'Iconv', 'jupiterx-core' ),
			'value' => class_exists( 'Iconv' ) ? $value['enabled'] : $value['disabled'],
		];

		if ( defined( 'WP_HTTP_BLOCK_EXTERNAL' ) ) {
			$block_external = __( 'HTTP requests have been blocked by the WP_HTTP_BLOCK_EXTERNAL constant, with no allowed hosts.', 'jupiterx-core' );

			if ( defined( 'WP_ACCESSIBLE_HOSTS' ) ) {
				$allowed_hosts = explode( ',', WP_ACCESSIBLE_HOSTS );
			}

			if ( isset( $allowed_hosts ) && count( $allowed_hosts ) > 0 ) {
				$block_external = sprintf(
					/* translators: 1: Allowed hosts */
					esc_html__( 'HTTP requests have been blocked by the WP_HTTP_BLOCK_EXTERNAL constant, with some hosts whitelisted: %s.', 'jupiterx-core' ),
					implode( ',', $allowed_hosts )
				);
			}
		}

		$info['wp-server']['fields']['http_requests'] = [
			'label' => __( 'HTTP Requests', 'jupiterx-core' ),
			'value' => isset( $block_external ) ? $block_external : __( 'Accessible', 'jupiterx-core' ),
		];

		$artbees_dotnet = wp_remote_get( 'https://artbees.net', [ 'timeout' => 10 ] );

		if ( ! is_wp_error( $artbees_dotnet ) ) {
			$info['wp-server']['fields']['artbees_communication'] = [
				'label' => __( 'Communication with Artbees' ),
				'value' => __( 'artbees.net is reachable' ),
				'debug' => 'true',
			];
		} else {
			$info['wp-server']['fields']['artbees_communication'] = [
				'label' => __( 'Communication with artbees.net' ),
				'value' => sprintf(
					/* translators: 1: The IP address artbees.net resolves to. 2: The error returned by the lookup. */
					__( 'Unable to reach Artbees at %1$s: %2$s' ),
					gethostbyname( 'artbees.net' ),
					$artbees_dotnet->get_error_message()
				),
				'debug' => $artbees_dotnet->get_error_message(),
			];
		}

		// Database section.
		$tables = $this->get_tables_sizes();

		foreach ( $tables as $table ) {
			$info['wp-database']['fields'][ $table['name'] ] = [
				'label' => esc_html( $table['name'] ),
				'value' => $table['size'] . ' MB',
			];
		}

		// Browser section.
		if ( ! class_exists( 'Browser' ) ) {
			jupiterx_core()->load_files( [
				'control-panel/includes/class-browser',
			] );
		}

		$browser = new \Browser();

		$info['browser'] = [
			'label'  => __( 'Browser', 'jupiterx-core' ),
			'fields' => [
				'browser'    => [
					'label' => __( 'Browser', 'jupiterx-core' ),
					'value' => $browser->getBrowser(),
				],
				'user_agent' => [
					'label' => __( 'User agent', 'jupiterx-core' ),
					'value' => $browser->getUserAgent(),
				],
				'version'    => [
					'label' => __( 'Version', 'jupiterx-core' ),
					'value' => $browser->getVersion(),
				],
				'platform'   => [
					'label' => __( 'Platform', 'jupiterx-core' ),
					'value' => $browser->getPlatform(),
				],
			],
		];

		return $info;
	}

	/**
	 * Get database tables sizes.
	 *
	 * @since 1.18.0
	 *
	 * @return array Tables list with name and size.
	 */
	public function get_tables_sizes() {
		global $wpdb;

		$tables = [
			'options',
			'links',
			'commentmeta',
			'term_relationships',
			'postmeta',
			'posts',
			'term_taxonomy',
			'terms',
			'comments',
			'termmeta',
			'usermeta',
			'users',
		];
		if ( is_multisite() ) {
			if ( ! is_super_admin() ) {
				// Omit usermeta and users tables.
				$tables = array_splice( $tables, -2 );
			}
			$tables[] = 'blogs';
			$tables[] = 'blogs_versions ';
			$tables[] = 'registration_log';
			$tables[] = 'signups';
			$tables[] = 'site';
			$tables[] = 'sitemeta';
		}

		foreach ( $tables as $key => $table ) {
			$tables[ $key ] = $wpdb->prefix . $table;
		}

		$names = "'" . implode( "','", $tables ) . "'";
		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared
		$query = $wpdb->prepare( "
			SELECT TABLE_NAME AS `name`, ROUND(((DATA_LENGTH + INDEX_LENGTH) / 1024 / 1024),2) AS `size`
			FROM information_schema.TABLES
			WHERE TABLE_SCHEMA = %s AND TABLE_NAME IN ($names)
		", $wpdb->dbname );
		// phpcs:enable

		// phpcs:ignore
		return $wpdb->get_results( $query, ARRAY_A );
	}

	/**
	 * Get site status.
	 *
	 * @since 1.18.0
	 *
	 * @return array Site status tests.
	 */
	public function site_status_tests( $tests ) {
		$tests['direct'] = array_merge( $tests['direct'], [
			'jupiterx_core_php_memory_limit' => [
				'label' => __( 'PHP Memory Limit is sufficient', 'jupiterx-core' ),
				'test'  => [ $this, 'get_test_php_memory_limit' ],
			],
			'jupiterx_core_php_modules' => [
				'label' => __( 'Required and recommended PHP modules are installed', 'jupiterx-core' ),
				'test'  => [ $this, 'get_test_php_modules' ],
			],
		] );

		return $tests;
	}

	/**
	 * Test PHP memory limit.
	 *
	 * @since 1.18.0
	 *
	 * @return array PHP memory limit result.
	 */
	public function get_test_php_memory_limit() {
		$result = [
			'label'       => __( 'PHP memory limit is sufficient', 'jupiterx-core' ),
			'status'      => 'good',
			'badge'       => [
				'label' => 'Jupiter X',
				'color' => 'blue',
			],
			'description' => sprintf(
				'<p>%s</p>',
				__( 'PHP memory limit is the maximum amount of memory in bytes that a script is allowed to allocate. At least 128M is required for the theme and 256M is recommended if you have enabled bunch of plugins to your site.', 'jupiterx-core' )
			),
			'actions'     => sprintf(
				'<a href="%s" target="_blank" rel="noopener noreferrer">%s <span aria-hidden="true" class="dashicons dashicons-external"></span></a>',
				'https://themes.artbees.net/docs/jupiter-x-server-requirements/',
				__( 'Learn more about Jupiter X server requirements', 'jupiterx-core' )
			),
			'test'        => 'jupiterx_core_php_memory_limit',
		];

		$memory_limit = ini_get( 'memory_limit' );
		if ( defined( 'WP_MEMORY_LIMIT' ) ) {
			$memory_limit = WP_MEMORY_LIMIT;
		}

		if ( preg_match( '/^(\d+)(.)$/', $memory_limit, $matches ) ) {
			if ( 'M' === $matches[2] ) {
				$memory_limit = $matches[1] * MB_IN_BYTES;
			} elseif ( 'K' === $matches[2] ) {
				$memory_limit = $matches[1] * KB_IN_BYTES;
			}
		}

		if ( $memory_limit < ( 128 * MB_IN_BYTES ) ) {
			$result['status'] = 'critical';
			$result['label']  = __( 'Your site has insufficient PHP memory limit', 'jupiterx-core' );

			if ( defined( 'WP_MEMORY_LIMIT' ) ) {
				$result['description'] .= '<p>';
				$result['description'] .= sprintf(
					// translators: %s Value of site's WP_MEMORY_LIMIT
					__( 'The <code>WP_MEMORY_LIMIT</code> with defined value of %s has been added to this website\'s configuration file or defined as default by WordPress.', 'jupiterx-core' ),
					WP_MEMORY_LIMIT
				);
				$result['description'] .= '</p>';
			}
		}

		return $result;
	}

	/**
	 * Test PHP modules.
	 *
	 * @since 1.18.0
	 *
	 * @return array PHP modules result.
	 *
	 * @SuppressWarnings(PHPMD.NPathComplexity)
	 */
	public function get_test_php_modules() {
		$result = [
			'label'       => __( 'Required and recommended PHP modules are installed', 'jupiterx-core' ),
			'status'      => 'good',
			'badge'       => [
				'label' => 'Jupiter X',
				'color' => 'blue',
			],
			'description' => sprintf(
				'<p>%s</p>',
				__( 'PHP modules perform most of the tasks on the server that make your site run. Any changes to these must be made by your server administrator.', 'jupiterx-core' )
			),
			'actions'     => sprintf(
				'<a href="%s" target="_blank" rel="noopener noreferrer">%s <span aria-hidden="true" class="dashicons dashicons-external"></span></a>',
				'https://themes.artbees.net/docs/jupiter-x-server-requirements/',
				__( 'Learn more about Jupiter X server requirements', 'jupiterx-core' )
			),
			'test'        => 'jupiterx_core_php_modules',
		];

		$modules = [
			'curl'        => [
				'function' => 'curl_version',
				'required' => true,
			],
			'mbstring'    => [
				'function' => 'mb_check_encoding',
				'required' => true,
			],
			'libxml'     => [
				'extension' => 'libxml',
				'required'  => true,
			],
			'xmlreader'   => [
				'extension'    => 'xmlreader',
				'required'     => true,
				'fallback_for' => 'mod_xml',
			],
			'simplexml'   => [
				'extension'    => 'simplexml',
				'required'     => true,
				'fallback_for' => 'mod_xml',
			],
			'xml'         => [
				'function' => 'xml_parse',
				'required' => true,
			],
			'fsockopen'   => [
				'function' => 'fsockopen',
				'required' => true,
			],
			'soap'        => [
				'class'    => 'SoapClient',
				'required' => true,
			],
			'domdocument' => [
				'class'    => 'DOMDocument',
				'required' => true,
			],
			'zip'         => [
				'class'    => 'ZipArchive',
				'required' => true,
			],
			'zlib'        => [
				'extension'    => 'zlib',
				'required'     => true,
				'fallback_for' => 'zip',
			],
			'gzip'        => [
				'function' => 'gzopen',
				'required' => false,
			],
			'iconv'       => [
				'function' => 'iconv',
				'required' => true,
			],
			'openssl'     => [
				'function' => 'openssl_encrypt',
				'required' => true,
			],
			'imagick'     => [
				'extension' => 'imagick',
				'required'  => true,
			],
			'gd'          => [
				'extension'    => 'gd',
				'required'     => true,
				'fallback_for' => 'imagick',
			],
		];

		$failures = [];

		foreach ( $modules as $library => $module ) {
			$extension  = ( isset( $module['extension'] ) ? $module['extension'] : null );
			$function   = ( isset( $module['function'] ) ? $module['function'] : null );
			$constant   = ( isset( $module['constant'] ) ? $module['constant'] : null );
			$class_name = ( isset( $module['class'] ) ? $module['class'] : null );

			// If this module is a fallback for another function, check if that other function passed.
			if ( isset( $module['fallback_for'] ) ) {
				/*
				 * If that other function has a failure, mark this module as required for normal operations.
				 * If that other function hasn't failed, skip this test as it's only a fallback.
				 */
				if ( isset( $failures[ $module['fallback_for'] ] ) ) {
					$module['required'] = true;
				} else {
					continue;
				}
			}

			if ( ! $this->test_php_extension_availability( $extension, $function, $constant, $class_name ) && ( ! isset( $module['php_bundled_version'] ) || version_compare( PHP_VERSION, $module['php_bundled_version'], '<' ) ) ) {
				if ( $module['required'] ) {
					$result['status'] = 'critical';

					$class         = 'error';
					$screen_reader = __( 'Error', 'jupiterx-core' );
					$message       = sprintf(
						/* translators: %s: The module name. */
						__( 'The required module, %s, is not installed, or has been disabled.', 'jupiterx-core' ),
						$library
					);
				} else {
					$class         = 'warning';
					$screen_reader = __( 'Warning', 'jupiterx-core' );
					$message       = sprintf(
						/* translators: %s: The module name. */
						__( 'The optional module, %s, is not installed, or has been disabled.', 'jupiterx-core' ),
						$library
					);
				}

				if ( ! $module['required'] && 'good' === $result['status'] ) {
					$result['status'] = 'recommended';
				}

				$failures[ $library ] = "<span class='dashicons $class'><span class='screen-reader-text'>$screen_reader</span></span> $message";
			}
		}

		if ( ! empty( $failures ) ) {
			$output = '<ul>';

			foreach ( $failures as $failure ) {
				$output .= sprintf(
					'<li>%s</li>',
					$failure
				);
			}

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

		if ( 'good' !== $result['status'] ) {
			if ( 'critical' === $result['status'] ) {
				$result['label'] = __( 'Required PHP modules are missing', 'jupiterx-core' );
			} else {
				$result['label'] = __( 'Recommended PHP modules are missing', 'jupiterx-core' );
			}

			$result['description'] .= sprintf(
				'<p>%s</p>',
				$output
			);
		}

		return $result;
	}

	/**
	 * Check if the passed extension or function are available.
	 *
	 * Make the check for available PHP modules into a simple boolean operator for a cleaner test runner.
	 *
	 * @since 1.18.0
	 *
	 * @param string $extension Optional. The extension name to test. Default null.
	 * @param string $function  Optional. The function name to test. Default null.
	 * @param string $constant  Optional. The constant name to test for. Default null.
	 * @param string $class     Optional. The class name to test for. Default null.
	 *
	 * @return bool Whether or not the extension and function are available.
	 *
	 * @SuppressWarnings(PHPMD.NPathComplexity)
	 */
	private function test_php_extension_availability( $extension = null, $function = null, $constant = null, $class = null ) {
		// If no extension or function is passed, claim to fail testing, as we have nothing to test against.
		if ( ! $extension && ! $function && ! $constant && ! $class ) {
			return false;
		}

		if ( $extension && ! extension_loaded( $extension ) ) {
			return false;
		}
		if ( $function && ! function_exists( $function ) ) {
			return false;
		}
		if ( $constant && ! defined( $constant ) ) {
			return false;
		}
		if ( $class && ! class_exists( $class ) ) {
			return false;
		}

		return true;
	}
}

new JupiterX_Core_Site_Health();
