<?php
/**
 * MainWP Child Vulnerability Checker
 *
 * MainWP Vulnerability Checker Extension handler.
 *
 * @link https://mainwp.com/extension/vulnerability-checker/
 *
 * @package MainWP\Child
 *
 * Credits
 *
 * Plugin-Name: Vulnerability Alerts
 * Plugin URI: http://wordpress.org/plugins/vulnerability-alerts/
 * Author: Edir Pedro
 * Author URI: http://edirpedro.com.br
 * License: GPL2
 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
 */

namespace MainWP\Child;

/**
 * Class MainWP_Child_Vulnerability_Checker
 *
 * MainWP Vulnerability Checker Extension handler.
 */
class MainWP_Child_Vulnerability_Checker {

	/**
	 * Public static variable to hold the single instance of the class.
	 *
	 * @var mixed Default null
	 */
	public static $instance = null;

	/**
	 * WPVulnDB API URL
	 *
	 * @var string
	 */
	private $wpvulndb_api = 'https://wpvulndb.com/api/v3/';

	/**
	 * Whether or not to use wpvulndb_token. Default: false.
	 *
	 * @var bool true|false
	 */
	private $wpvulndb_token = false;

	/**
	 * Method instance()
	 *
	 * Create a public static instance.
	 *
	 * @return mixed Class instance.
	 */
	public static function instance() {
		if ( null === self::$instance ) {
				self::$instance = new self();
		}
			return self::$instance;
	}

	/**
	 * MainWP_Child_Vulnerability_Checker constructor.
	 *
	 * Run any time class is called.
	 */
	public function __construct() {
		$this->wpvulndb_token = get_option( 'mainwp_child_wpvulndb_token', '' );
	}

	/**
	 * MainWP Child Vulnerability Checker actions.
	 *
	 * @uses \MainWP\Child\MainWP_Helper::write()
	 */
	public function action() {
		$information = array();
		if ( ! empty( $this->wpvulndb_token ) ) {
			if ( isset( $_POST['mwp_action'] ) ) {
				$mwp_action = ! empty( $_POST['mwp_action'] ) ? sanitize_text_field( wp_unslash( $_POST['mwp_action'] ) ) : '';
				switch ( $mwp_action ) {
					case 'vulner_recheck':
						$information = $this->vulner_recheck();
						break;
				}
			}
		}

		MainWP_Helper::write( $information );
	}

	/**
	 * Check for vulnerabilities.
	 *
	 * @return array Return an array $information[] containing $results[] array.
	 */
	public function vulner_recheck() {
			$result           = array();
			$force            = ( isset( $_POST['force'] ) && ! empty( $_POST['force'] ) ) ? true : false;
			$result['plugin'] = $this->check_plugins( $force );
			$result['wp']     = $this->check_wp( $force );
			$result['theme']  = $this->check_themes( $force );
			$information      = array(
				'result' => $result,
				'ok'     => 1,
			);
			return $information;
	}

	/**
	 * Check for plugin vulnerabilities.
	 *
	 * @param bool $force Whether or not to force check. Default: false.
	 * @return array Return $result array.
	 */
	public function check_plugins( $force = false ) {
			$result         = array();
			$active_plugins = get_option( 'active_plugins' );

		if ( ! empty( $active_plugins ) ) {
			foreach ( $active_plugins as $plug ) {

				$plugin_file    = WP_CONTENT_DIR . '/plugins/' . $plug;
				$plugin_info    = get_plugin_data( $plugin_file );
				$plugin_version = isset( $plugin_info['Version'] ) ? $plugin_info['Version'] : '';
				$string         = explode( '/', $plug );
				$plug_vuln      = get_transient( 'mainwp_vulnche_trans_plug_' . $string[0] );
				if ( false === $plug_vuln || $force ) {
					$plug_vuln = $this->vulnche_get_content( $this->wpvulndb_api . 'plugins/' . $string[0] );
					set_transient( 'mainwp_vulnche_trans_plug_' . $string[0], $plug_vuln, 1 * DAY_IN_SECONDS );
				}
				if ( $plug_vuln ) {
					$plug_vuln        = json_decode( $plug_vuln, true );
					$plug_vuln_filter = $plug_vuln;
					foreach ( $plug_vuln as $slug => $pl_data ) {
						if ( isset( $pl_data['vulnerabilities'] ) && count( $pl_data['vulnerabilities'] ) > 0 ) {
							$plug_vulner_data = array();
							foreach ( $pl_data['vulnerabilities'] as $vuln_data ) {
								if ( isset( $vuln_data['fixed_in'] ) && version_compare( $plugin_version, $vuln_data['fixed_in'] ) >= 0 ) {
									continue;
								}
								$plug_vulner_data[] = $vuln_data;
							}

							if ( 0 == count( $plug_vulner_data ) ) {
								unset( $plug_vuln_filter[ $slug ] );
							} else {
								$plug_vuln_filter[ $slug ]['vulnerabilities']  = $plug_vulner_data;
								$plug_vuln_filter[ $slug ]['detected_version'] = $plugin_version;
								$plug_vuln_filter[ $slug ]['plugin_slug']      = $plug;
							}
						} else {
							unset( $plug_vuln_filter[ $slug ] );
						}
					}

					if ( 0 == count( $plug_vuln_filter ) ) {
						continue;
					}
					$plug_vuln = wp_json_encode( $plug_vuln_filter );
				} else {
					continue;
				}
				$result[ $plug ] = $plug_vuln;
			}
		}
		return $result;
	}

	/**
	 * Check for WP vulnerabilities.
	 *
	 * @param bool $force Whether or not to force check. Default: false.
	 * @return bool|string|null
	 */
	public function check_wp( $force = false ) {
			$wp_vuln    = get_transient( 'mainwp_vulnche_trans_wp_json' );
			$wp_version = str_replace( '.', '', get_bloginfo( 'version' ) );
		if ( false === $wp_vuln || $force ) {
			$wp_vuln = $this->vulnche_get_content( $this->wpvulndb_api . 'wordpresses/' . $wp_version );
			set_transient( 'mainwp_vulnche_trans_wp_json', $wp_vuln, 1 * DAY_IN_SECONDS );
		}
		return $wp_vuln;
	}

	/**
	 * Check if themes have vunerabilities.
	 *
	 * @param bool $force Whether or not to force check. Default: false.
	 * @return array Return $result array.
	 */
	public function check_themes( $force = false ) {

		include_once ABSPATH . '/wp-admin/includes/update.php';
		require_once ABSPATH . 'wp-admin/includes/misc.php';
		require_once ABSPATH . 'wp-admin/includes/theme.php';

		if ( current_user_can( 'switch_themes' ) ) {
			$themes = wp_prepare_themes_for_js();
		} else {
			$themes = wp_prepare_themes_for_js( array( wp_get_theme() ) );
		}
		wp_reset_vars( array( 'theme', 'search' ) );
		$result = array();
		if ( ! empty( $themes ) ) {
			foreach ( $themes as $th ) {
				if ( empty( $th['parent'] ) ) {
					$th_vuln = get_transient( 'mainwp_vulnche_trans_theme_' . $th['id'] );
					if ( false === $th_vuln || $force ) {
						$th_vuln = $this->vulnche_get_content( $this->wpvulndb_api . 'themes/' . $th['id'] );
						set_transient( 'mainwp_vulnche_trans_theme_' . $th['id'], $th_vuln, 1 * DAY_IN_SECONDS );
					}

					if ( $th_vuln ) {
						$th_vuln        = json_decode( $th_vuln, true );
						$th_vuln_filter = $th_vuln;
						foreach ( $th_vuln as $slug => $th_data ) {
							if ( isset( $th_data['vulnerabilities'] ) && count( $th_data['vulnerabilities'] ) > 0 ) {
								$th_vulner_data = array();
								foreach ( $th_data['vulnerabilities'] as $vuln_data ) {
									if ( empty( $vuln_data ) ) {
										continue;
									}
									if ( isset( $vuln_data['fixed_in'] ) && version_compare( $th['version'], $vuln_data['fixed_in'] ) >= 0 ) {
										continue;
									}
									$th_vulner_data[] = $vuln_data;
								}
								if ( 0 == count( $th_vulner_data ) ) {
									unset( $th_vuln_filter[ $slug ] );
								} else {
									$th_vuln_filter[ $slug ]['vulnerabilities'] = $th_vulner_data;
								}
							} else {
								unset( $th_vuln_filter[ $slug ] );
							}
						}
						if ( 0 == count( $th_vuln_filter ) ) {
							continue;
						}
						$th_vuln = wp_json_encode( $th_vuln_filter );
					} else {
						continue;
					}
					$result[ $th['id'] ]['vulner_data']      = $th_vuln;
					$result[ $th['id'] ]['name']             = $th['name'];
					$result[ $th['id'] ]['author']           = $th['author'];
					$result[ $th['id'] ]['detected_version'] = $th['version'];
				}
			}
		}
		return $result;
	}

	/**
	 * Check if content is vulnerable.
	 *
	 * @param string $url URL to check.
	 *
	 * @return bool|string|null
	 */
	public function vulnche_get_content( $url ) {

		// phpcs:disable WordPress.WP.AlternativeFunctions -- Required to achieve desired results, pull request solutions appreciated.
		$ch = curl_init();
		curl_setopt( $ch, CURLOPT_URL, $url );
		curl_setopt( $ch, CURLOPT_HEADER, 0 );
		curl_setopt( $ch, CURLOPT_HTTPHEADER, array( 'Authorization: Token token=' . $this->wpvulndb_token ) );
		curl_setopt( $ch, CURLOPT_USERAGENT, $this->get_random_user_agent() );
		curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );

		$output = curl_exec( $ch );
		$info   = curl_getinfo( $ch, CURLINFO_HTTP_CODE );

		curl_close( $ch );
		// phpcs:enable

		if ( false === $output || 200 != $info ) {
			$output = null;
		}
		return $output;
	}

	/**
	 * Get random useragent.
	 *
	 * @return string User agent string.
	 */
	public function get_random_user_agent() {
		$someUA = array(
			'Mozilla/5.0 (Windows; U; Windows NT 6.0; fr; rv:1.9.1b1) Gecko/20081007 Firefox/3.1b1',
			'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.0.1) Gecko/2008070208 Firefox/3.0.0',
			'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.19 (KHTML, like Gecko) Chrome/0.4.154.18 Safari/525.19',
			'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.13 (KHTML, like Gecko) Chrome/0.2.149.27 Safari/525.13',
			'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30)',
			'Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.1; .NET CLR 1.1.4322; .NET CLR 2.0.40607)',
			'Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.1; .NET CLR 1.1.4322)',
			'Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.1; .NET CLR 1.0.3705; Media Center PC 3.1; Alexa Toolbar; .NET CLR 1.1.4322; .NET CLR 2.0.50727)',
			'Mozilla/45.0 (compatible; MSIE 6.0; Windows NT 5.1)',
			'Mozilla/4.08 (compatible; MSIE 6.0; Windows NT 5.1)',
			'Mozilla/4.01 (compatible; MSIE 6.0; Windows NT 5.1)',
		);
		$i      = wp_rand( 0, count( $someUA ) - 1 );
		return $someUA[ $i ];
	}


}
