<?php

final class ITSEC_Malware_Scanner {
	protected static $transient_name = 'itsec_cached_sucuri_scan';

	public static function scan( $site_id = 0 ) {

		if ( ( $results = apply_filters( 'itsec_pre_malware_scanner_scan', null, $site_id ) ) !== null ) {
			return $results;
		}

		$process_id = ITSEC_Log::add_process_start( 'malware', 'scan', compact( 'site_id' ) );

		if ( $site_id && ! is_main_site( $site_id ) ) {
			$results = self::scan_sub_site( $site_id, $process_id );
		} else {
			$results = self::scan_main_site( $process_id );
		}

		ITSEC_Log::add_process_stop( $process_id, compact( 'results' ) );

		if ( is_array( $results ) && ! empty( $results['cached'] ) ) {
			return $results;
		}

		if ( is_wp_error( $results ) ) {
			if ( self::is_sucuri_error( $results ) ) {
				ITSEC_Log::add_warning( 'malware', 'scan-failure-server-error', compact( 'results' ) );
			} else {
				ITSEC_Log::add_warning( 'malware', 'scan-failure-client-error', compact( 'results' ) );
			}
		} else if ( ! empty( $results['SYSTEM']['ERROR'] ) ) {
			ITSEC_Log::add_warning( 'malware', 'sucuri-system-error', compact( 'results' ) );
		} else if ( ! empty( $results['MALWARE']['WARN'] ) ) {
			$data = compact( 'results' );

			if ( ITSEC_Lib_Remote_Messages::has_action( 'malware-scanner-disable-malware-warnings' ) ) {
				$data['sucuri_error'] = true;
				ITSEC_Log::add_warning( 'malware', 'malware-warning-suppressed', $data );

				return $results;
			}

			if ( ! empty( $results['BLACKLIST']['WARN'] ) ) {
				ITSEC_Log::add_critical_issue( 'malware', 'found-malware-and-on-blacklist', $data );
			} else {
				ITSEC_Log::add_critical_issue( 'malware', 'found-malware', $data );
			}
		} else if ( ! empty( $results['BLACKLIST']['WARN'] ) ) {
			ITSEC_Log::add_critical_issue( 'malware', 'on-blacklist', compact( 'results' ) );
		} else {
			ITSEC_Log::add_notice( 'malware', 'clean', compact( 'results' ) );
		}

		return $results;
	}

	/**
	 * This attempts to determine if this is a temporary Sucuri error or something the user needs to take action on.
	 *
	 * @param WP_Error|array $results
	 *
	 * @return bool
	 */
	public static function is_sucuri_error( $results ) {
		if ( ! is_wp_error( $results ) ) {
			return false;
		}

		$code = $results->get_error_code();

		if ( 'http_request_failed' === $code && strpos( $results->get_error_message(), 'cURL error 52:' ) !== false ) {
			return true;
		}

		// Networking error probably due to a server issue.
		if ( strpos( $code, 'itsec' ) === false ) {
			return false;
		}

		$plugin_conflict_codes = array(
			'itsec-malware-scanner-wp-remote-get-response-malformed',
			'itsec-malware-scanner-wp-remote-get-response-missing-body',
			'itsec-malware-scanner-wp-remote-get-response-empty-body',
		);

		// Probably a plugin conflict.
		if ( in_array( $code, $plugin_conflict_codes, true ) ) {
			return false;
		}

		return true;
	}

	/**
	 * Scan the main site of the network.
	 *
	 * This handles caching and filter overrides for URLs.
	 *
	 * @param array $process_id
	 *
	 * @return array|WP_Error
	 */
	protected static function scan_main_site( $process_id ) {

		$response = get_site_transient( self::$transient_name );
		$cached = true;

		ITSEC_Log::add_process_update( $process_id, array( 'cached-response' => $response ) );

		if ( defined( 'ITSEC_TEST_MALWARE_SCAN_SKIP_CACHE' ) && ITSEC_TEST_MALWARE_SCAN_SKIP_CACHE ) {
			ITSEC_Log::add_process_update( $process_id, array( 'action' => 'define-skip-cache', 'define-name' => 'ITSEC_TEST_MALWARE_SCAN_SKIP_CACHE', 'define-value' => ITSEC_TEST_MALWARE_SCAN_SKIP_CACHE ) );
			$cached = false;
			$response = false;
		}

		if ( false === $response ) {
			$cached = false;

			$site_url = apply_filters( 'itsec_test_malware_scan_site_url', get_home_url() );

			if ( defined( 'ITSEC_TEST_MALWARE_SCAN_SITE_URL' ) ) {
				ITSEC_Log::add_process_update( $process_id, array( 'action' => 'define-force-site-url', 'original-site-url' => $site_url, 'define-name' => 'ITSEC_TEST_MALWARE_SCAN_SITE_URL', 'define-value' => ITSEC_TEST_MALWARE_SCAN_SITE_URL ) );
				$site_url = ITSEC_TEST_MALWARE_SCAN_SITE_URL;
			}

			$response = self::scan_url( $site_url, $process_id );

			if ( ! is_wp_error( $response ) ) {
				set_site_transient( self::$transient_name, array(
					'body'     => $response['body'],
					'headers'  => $response['headers'],
					'response' => $response['response'],
				), MINUTE_IN_SECONDS * 10 );
			}
		} else {
			ITSEC_Log::add_process_update( $process_id, array( 'action' => 'using-cached-response' ) );
		}

		if ( is_wp_error( $response ) ) {
			return $response;
		}

		$results = self::parse_response( $response );

		if ( ! is_wp_error( $results ) ) {
			$results['cached'] = $cached;
		}

		return $results;
	}

	/**
	 * Scan a sub site.
	 *
	 * These requests are not cached.
	 *
	 * @param int   $site_id
	 * @param array $process_id
	 *
	 * @return array|WP_Error
	 */
	protected static function scan_sub_site( $site_id, $process_id ) {

		$url    = get_home_url( $site_id );
		$record = array(
			'url' => $url,
			'id'  => $site_id,
		);

		if ( $url ) {
			$response = self::scan_url( $url, $process_id );

			if ( is_wp_error( $response ) ) {
				return $response;
			}

			$results = self::parse_response( $response );
		} else {
			$results = new WP_Error( 'itsec-malware-scanner-invalid-site', sprintf( __( 'Failed to scan site #%d because it does not exist.', 'better-wp-security' ), $site_id ) );
		}

		if ( is_wp_error( $results ) ) {
			$results->add_data( array_merge( $results->get_error_data(), array( 'itsec_site' => $record ) ) );
		} else {
			$results['itsec_site'] = $record;
		}

		return $results;
	}

	/**
	 * Request a Sucuri Malware Scan for a URL.
	 *
	 * @param string $site_url
	 * @param array  $process_id
	 *
	 * @return array|WP_Error
	 */
	protected static function scan_url( $site_url, $process_id ) {

		$site_url = preg_replace( '|^https?://|i', '', $site_url );

		$query_args = array(
			'scan'  => $site_url,
			'p'     => 'ithemes',
			'clear' => 1,
			'json'  => 1,
			'time'  => time(),
		);

		$key = apply_filters( 'itsec_sucuri_key', '' );

		if ( defined( 'ITSEC_SUCURI_KEY' ) ) {
			$key = ITSEC_SUCURI_KEY;
		}

		if ( ! empty( $key ) ) {
			$query_args['k'] = $key;
		}

		$scanner_url = 'https://sitecheck.sucuri.net/';
		$scan_url = "$scanner_url?" . http_build_query( $query_args, '', '&' );

		$req_args = array(
			'connect_timeout' => 30, // Placeholder for when WordPress implements support.
			'timeout'         => 300,
		);

		if ( defined( 'ITSEC_TEST_MALWARE_SCAN_DISABLE_SSLVERIFY' ) && ITSEC_TEST_MALWARE_SCAN_DISABLE_SSLVERIFY ) {
			$req_args['sslverify'] = false;

			// Ensure that another plugin isn't preventing the disabling of sslverify from working.
			add_filter( 'https_local_ssl_verify', '__return_false', 999999 );
			add_filter( 'https_ssl_verify', '__return_false', 999999 );
		}

		$response = wp_remote_get( $scan_url, $req_args );

		if ( defined( 'ITSEC_TEST_MALWARE_SCAN_DISABLE_SSLVERIFY' ) && ITSEC_TEST_MALWARE_SCAN_DISABLE_SSLVERIFY ) {
			remove_filter( 'https_local_ssl_verify', '__return_false', 999999 );
			remove_filter( 'https_ssl_verify', '__return_false', 999999 );
		}

		ITSEC_Log::add_process_update( $process_id, compact( 'scan_url', 'req_args', 'response' ) );

		return $response;
	}

	/**
	 * Parse the response from Sucuri to get either a WP_Error instance or the scan results.
	 *
	 * @param array $response
	 *
	 * @return array|WP_Error
	 */
	protected static function parse_response( $response ) {

		if ( isset( $response['body'] ) ) {
			$body = $response['body'];
		} else {
			return new WP_Error( 'itsec-malware-scanner-wp-remote-get-response-missing-body', __( 'The scan failed due to an unexpected technical error. The response from the wp_remote_get function does not contain a body entry. Since the body entry contains the response for the request to Sucuri\'s servers, the response cannot be processed. This could indicate a plugin/theme compatibility issue or a problem in WordPress.', 'better-wp-security' ), $response );
		}

		if ( empty( $body ) ) {
			return new WP_Error( 'itsec-malware-scanner-wp-remote-get-response-empty-body', __( 'The scan failed due to an unexpected technical error. The response from the wp_remote_get function contains an empty body entry. Since the body entry contains the response for the request to Sucuri\'s servers, the response cannot be processed. This could indicate a plugin/theme compatibility issue or a problem in WordPress.', 'better-wp-security' ), $response );
		}

		$body = @json_decode( $body, true );

		if ( is_null( $body ) && isset( $response['headers']['content-type'] ) ) {
			if ( 'application/json' === $response['headers']['content-type'] ) {
				return new WP_Error( 'itsec-malware-scanner-invalid-json-data-in-scan-response', __( 'The scan did not complete successfully. The Sucuri server should send its response in JSON encoding. The response indicates that the encoding is JSON, but the data could not be decoded. This problem could be due to a temporary Sucuri server issue or a compatibility issue on your server. If the problem continues, please contact iThemes Security support.', 'better-wp-security' ), $response );
			} else {
				return new WP_Error( 'itsec-malware-scanner-invalid-content-type-in-scan-response', sprintf( __( 'The scan did not complete successfully. The Sucuri server should send its response in JSON encoding. The data received from the Sucuri server could not be decoded. In addition, a content type of <code>%s</code> was received when a content type of <code>application/json</code> was expected. This could indicate a temporary issue with the Sucuri servers.', 'better-wp-security' ), esc_html( $response['headers']['content-type'] ) ), $response );
			}
		} else if ( ! is_array( $body ) ) {
			if ( 0 === strpos( $response['body'], 'ERROR' ) ) {
				return new WP_Error( 'itsec-malware-scanner-error-received', sprintf( __( 'The scan did not complete successfully. Sucuri sent the following error: %s', 'better-wp-security' ), '<code>' . $response['body'] . '</code>' ), $response );
			}

			if ( ! empty( $response['response'] ) && ! empty( $response['response']['code'] ) ) {
				return new WP_Error( 'itsec-malware-scanner-unknown-scan-error', sprintf( __( 'An unknown error prevented the scan from completing successfully. The Sucuri server responded with a <code>%s</code> error code.', 'better-wp-security' ), $response['response']['code'] ), $response );
			}

			return new WP_Error( 'itsec-malware-scanner-wp-remote-get-response-malformed', __( 'The scan failed due to an unexpected technical error. The response from the wp_remote_get function is missing some critical information that is needed in order to properly process the response from Sucuri\'s servers. This could indicate a plugin/theme compatibility issue or a problem in WordPress.', 'better-wp-security' ), $response );
		}

		return $body;
	}
}
