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

	/**
	 * Nvd.nist.gov API URL
	 *
	 * @var string
	 */
	private $wpvulndb_nvd_api = 'https://services.nvd.nist.gov/rest/json/cves/1.0';


	/**
	 * 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() {
		$wpvulndb_token = get_option( 'mainwp_child_wpvulndb_token', '' );
		if ( ! empty( $wpvulndb_token ) ) {
			delete_option( 'mainwp_child_wpvulndb_token' ); // old value.
		}
	}

	/**
	 * MainWP Child Vulnerability Checker actions.
	 *
	 * @uses \MainWP\Child\MainWP_Helper::write()
	 */
	public function action() {
		$information    = array();
			$mwp_action = MainWP_System::instance()->validate_params( 'mwp_action' );
		if ( ! empty( $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();
		// phpcs:disable WordPress.Security.NonceVerification
		$force   = ( isset( $_POST['force'] ) && ! empty( $_POST['force'] ) ) ? true : false;
		$service = ( isset( $_POST['service'] ) && 'nvd_nist' === $_POST['service'] ) ? 'nvd_nist' : 'wpvulndb';

		if ( 'wpvulndb' === $service ) {
			if ( empty( $_POST['wpvulndb_tk'] ) ) {
				return array( 'error' => 'Error: empty WPScan API token.' );
			}
			$encrypted = MainWP_Child_Keys_Manager::instance()->encrypt_string( sanitize_text_field( wp_unslash( $_POST['wpvulndb_tk'] ) ) );
			set_transient( 'mainwp_child_trans_wpvulndb_tk', $encrypted, 30 * MINUTE_IN_SECONDS );
		}
		// phpcs:enable

		$result['plugin'] = $this->check_plugins( $force, $service );
		$result['wp']     = $this->check_wp( $force, $service );
		$result['theme']  = $this->check_themes( $force, $service );
		$information      = array(
			'result' => $result,
			'ok'     => 1,
		);
		return $information;
	}

	/**
	 * Check for plugin vulnerabilities.
	 *
	 * @param bool $force Whether or not to force check. Default: false.
	 * @param bool $service Selected service.
	 *
	 * @return array Return $result array.
	 */
	public function check_plugins( $force = false, $service = '' ) {
			$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 ) {

					if ( 'nvd_nist' === $service ) {
						if ( 'wordfence' === strtolower( $string[0] ) ) {
							continue;
						}
						$url = $this->wpvulndb_nvd_api . '?keyword=' . $string[0];
					} else {
						$url = $this->wpvulndb_api . 'plugins/' . $string[0];
					}
					$plug_vuln = $this->vulnche_get_content( $url, $service );
					set_transient( 'mainwp_vulnche_trans_plug_' . $string[0], $plug_vuln, 1 * DAY_IN_SECONDS );
				}
				if ( $plug_vuln ) {
					$plug_vuln_filter = array();
					if ( 'nvd_nist' === $service ) {
						$plug_vuln_filter = $this->get_vuln_nvd_nist_info( $plug_vuln, $string[0], $plugin_version );
					} else {
						$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.
	 * @param bool $service Selected service.
	 * @return bool|string|null
	 */
	public function check_wp( $force = false, $service = '' ) {
		$wp_vuln        = get_transient( 'mainwp_vulnche_trans_wp_json' );
		$wp_version     = get_bloginfo( 'version' );
		$number_version = str_replace( '.', '', $wp_version );
		if ( false === $wp_vuln || $force ) {
			if ( 'nvd_nist' === $service ) {
				$url = $this->wpvulndb_nvd_api . '?cpeMatchString=cpe:2.3:a:WordPress:WordPress';
			} else {
				$url = $this->wpvulndb_api . 'wordpresses/' . $number_version;
			}
			$wp_vuln = $this->vulnche_get_content( $url, $service );

			if ( 'nvd_nist' === $service ) {
				$wp_vuln = $this->get_vuln_nvd_nist_info( $wp_vuln, 'wordpress', $wp_version ); //phpcs:ignore -- wordpress.
				$wp_vuln = wp_json_encode( $wp_vuln );
			}
			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.
	 * @param bool $service Selected service.
	 *
	 * @return array Return $result array.
	 */
	public function check_themes( $force = false, $service = '' ) {  // phpcs:ignore -- ignore complex method notice.

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

						if ( 'nvd_nist' === $service ) {
							$url = $this->wpvulndb_nvd_api . '?keyword=' . $th['id'];
						} else {
							$url = $this->wpvulndb_api . 'themes/' . $th['id'];
						}

						$th_vuln = $this->vulnche_get_content( $url, $service );
						set_transient( 'mainwp_vulnche_trans_theme_' . $th['id'], $th_vuln, 1 * DAY_IN_SECONDS );
					}

					if ( $th_vuln ) {
						if ( 'nvd_nist' === $service ) {
							$th_vuln_filter = $this->get_vuln_nvd_nist_info( $th_vuln, $th['id'], $th['version'] );
						} else {
							$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 mixed  $data data of check.
	 * @param string $slug slug.
	 * @param string $version Current version.
	 *
	 * @return bool|string|null
	 */
	public function get_vuln_nvd_nist_info( $data, $slug, $version ) {  // phpcs:ignore -- ignore complex method notice.
		$data = json_decode( $data, true );

		$filtered_data = array();
		if ( is_array( $data ) && isset( $data['result'] ) && isset( $data['result']['CVE_Items'] ) ) {
			$vulns = array();

			foreach ( $data['result']['CVE_Items'] as $item ) {
				$info = array();

				if ( isset( $item['publishedDate'] ) ) {
					$info['date'] = $item['publishedDate'];
				}

				if ( isset( $item['cve']['description']['description_data'][0]['value'] ) ) {
					$info['detail'] = $item['cve']['description']['description_data'][0]['value'];
				}

				$version_missed = true;
				if ( ! empty( $info ) ) {
					$info['cve_id'] = $item['cve']['CVE_data_meta']['ID'];
					$info['slug']   = $slug;

					$fixed = false;
					if ( 'wordpress' === $slug ) { //phpcs:ignore -- wordpress.
						$founded = false;
						if ( isset( $item['configurations'] ) && isset( $item['configurations']['nodes'] ) ) {
							foreach ( $item['configurations']['nodes'] as $conf_node ) {
								if ( isset( $conf_node['cpe_match'] ) ) {
									foreach ( $conf_node['cpe_match'] as $cpe_match ) {
										if ( ! isset( $cpe_match['cpe23Uri'] ) || false === strpos( $cpe_match['cpe23Uri'], 'cpe:2.3:a:WordPress:WordPress' ) ) {
											continue;
										}
										$founded = true;
										if ( isset( $cpe_match['versionEndExcluding'] ) ) {
											$version_end_excluding = $cpe_match['versionEndExcluding'];
											if ( version_compare( $version, $version_end_excluding ) >= 0 ) {
												$fixed = true; // to continue.
											}
											$version_missed = false;
										} elseif ( isset( $cpe_match['versionEndIncluding'] ) ) {
											$version_end_including = $cpe_match['versionEndIncluding'];
											if ( version_compare( $version, $version_end_including ) >= 0 ) {
												$fixed = true; // to continue.
											}
											$version_missed = false;
										}
										if ( $fixed ) {
											break;
										}
									}
								}

								if ( ! $fixed && isset( $conf_node['children'] ) ) {
									foreach ( $conf_node['children'] as $child_node ) {
										if ( isset( $child_node['cpe_match'] ) ) {
											foreach ( $child_node['cpe_match'] as $cpe_match ) {
												if ( ! isset( $cpe_match['cpe23Uri'] ) || false === strpos( $cpe_match['cpe23Uri'], 'cpe:2.3:a:WordPress:WordPress' ) ) {
													continue;
												}
												$founded = true;
												if ( isset( $cpe_match['versionEndExcluding'] ) ) {
													$version_end_excluding = $cpe_match['versionEndExcluding'];
													if ( version_compare( $version, $version_end_excluding ) >= 0 ) {
														$fixed = true; // to continue.
													}
													$version_missed = false;
												} elseif ( isset( $cpe_match['versionEndIncluding'] ) ) {
													$version_end_including = $cpe_match['versionEndIncluding'];
													if ( version_compare( $version, $version_end_including ) >= 0 ) {
														$fixed = true; // to continue.
													}
													$version_missed = false;
												}
												if ( $fixed ) {
													break;
												}
											}
										}
									}
								}
							}
						}
						if ( ! $founded ) {
							continue;
						}
					} elseif ( isset( $item['configurations'] ) && isset( $item['configurations']['nodes'] ) ) {
						foreach ( $item['configurations']['nodes'] as $conf_node ) {
							if ( isset( $conf_node['cpe_match'] ) ) {
								foreach ( $conf_node['cpe_match'] as $cpe_match ) {

									if ( isset( $cpe_match['versionEndExcluding'] ) ) {
										$version_end_excluding = $cpe_match['versionEndExcluding'];
										if ( version_compare( $version, $version_end_excluding ) >= 0 ) {
											$fixed = true; // to continue.
										}
										$version_missed = false;
									} elseif ( isset( $cpe_match['versionEndIncluding'] ) ) {
										$version_end_including = $cpe_match['versionEndIncluding'];
										if ( version_compare( $version, $version_end_including ) >= 0 ) {
											$fixed = true; // to continue.
										}
										$version_missed = false;
									}
									if ( $fixed ) {
										break;
									}
								}
							}

							if ( ! $fixed && isset( $conf_node['children'] ) ) {
								foreach ( $conf_node['children'] as $child_node ) {
									if ( isset( $child_node['cpe_match'] ) ) {
										foreach ( $child_node['cpe_match'] as $cpe_match ) {
											if ( isset( $cpe_match['versionEndExcluding'] ) ) {
												$version_end_excluding = $cpe_match['versionEndExcluding'];
												if ( version_compare( $version, $version_end_excluding ) >= 0 ) {
													$fixed = true; // to continue.
												}
												$version_missed = false;
											} elseif ( isset( $cpe_match['versionEndIncluding'] ) ) {
												$version_end_including = $cpe_match['versionEndIncluding'];
												if ( version_compare( $version, $version_end_including ) >= 0 ) {
													$fixed = true; // to continue.
												}
												$version_missed = false;
											}
											if ( $fixed ) {
												break;
											}
										}
									}
								}
							}
						}
					}

					// put here.
					if ( $fixed ) {
						continue;
					}
					if ( $version_missed ) {
						$info['missed_version'] = 1;
					}
					$vulns[] = $info;
				}
			}
			$filtered_data[ $slug ]['vulnerabilities'] = $vulns;
		}

		return $filtered_data;
	}

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

		$encrypted      = get_transient( 'mainwp_child_trans_wpvulndb_tk' );
		$wpvulndb_token = MainWP_Child_Keys_Manager::instance()->decrypt_string( $encrypted );

		// 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 );
		if ( 'nvd_nist' !== $service ) {
			if ( empty( $wpvulndb_token ) ) {
				if ( 'resource' === gettype( $ch ) ) {
					curl_close( $ch );
				}
				return false;
			}
			curl_setopt( $ch, CURLOPT_HTTPHEADER, array( 'Authorization: Token token=' . $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 );

		if ( 'resource' === gettype( $ch ) ) {
			curl_close( $ch );
		}
		// phpcs:enable

		if ( false === $output || 200 !== (int) $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 ];
	}
}
