<?php
/**
 * Represents a Malware Result behavior.
 *
 * @package WP_Defender\Behavior\Scan_Item
 */

namespace WP_Defender\Behavior\Scan_Item;

use WP_Error;
use WP_Defender\Traits\IO;
use WP_Defender\Model\Scan;
use WP_Defender\Traits\Theme;
use WP_Defender\Traits\Plugin;
use Calotes\Component\Behavior;
use WP_Defender\Traits\Formats;
use WP_Defender\Model\Scan_Item;
use WP_Defender\Component\Error_Code;
use WP_Defender\Traits\File_Operations;
use WP_Defender\Helper\File as File_Helper;
use WP_Defender\Component\Quarantine as Quarantine_Component;
use WP_Filesystem_Base;

/**
 * Represents a Malware Result behavior.
 */
class Malware_Result extends Behavior {

	use Formats;
	use IO;
	use Plugin;
	use Theme;
	use File_Operations;

	private const REMOTE_FILE_IDENTICAL  = 'REMOTE_FILE_IDENTICAL';
	private const REMOTE_FILE_DIFFER     = 'REMOTE_FILE_DIFFER';
	private const REMOTE_FILE_NOT_EXISTS = 'REMOTE_FILE_NOT_EXISTS';
	private const REPAIR_NOT_IMPLEMENTED = 'REPAIR_NOT_IMPLEMENTED';

	/**
	 * Generates an array representation of the scan item.
	 *
	 * @return array
	 */
	public function to_array() {
		$file = $this->owner->raw_data['file'];
		list( $file_size, $file_created_at, $delete ) = $this->get_file_meta( $file );
		$is_quarantinable                             = $delete ? false : $this->is_quarantinable( $file );

		$quarantine_data = class_exists( 'WP_Defender\Component\Quarantine' ) ?
			wd_di()->get( Quarantine_Component::class )->scan_item_data( $this->owner ) :
			array( 'is_quarantinable' => $is_quarantinable );

		return array_merge(
			array(
				'id'         => $this->owner->id,
				'type'       => Scan_Item::TYPE_SUSPICIOUS,
				'file_name'  => pathinfo( $file, PATHINFO_BASENAME ),
				'full_path'  => $file,
				'date_added' => $file_created_at,
				'size'       => $file_size,
				'deleted'    => $delete,
				'short_desc' => esc_html__( 'Suspicious function found', 'wpdef' ),
			),
			$quarantine_data
		);
	}

	/**
	 * Ignores the scan item and marks it as successfully ignored.
	 *
	 * @return array An array with a message indicating the successful ignore action.
	 */
	public function ignore(): array {
		$scan = Scan::get_last();
		$scan->ignore_issue( $this->owner->id );

		return array( 'message' => esc_html__( 'The suspicious file has been successfully ignored.', 'wpdef' ) );
	}

	/**
	 * Restores the ignored suspicious file.
	 *
	 * @return array An array with a message indicating the successful restoration action.
	 */
	public function unignore(): array {
		$scan = Scan::get_last();
		$scan->unignore_issue( $this->owner->id );

		return array( 'message' => esc_html__( 'The suspicious file has been successfully restored.', 'wpdef' ) );
	}

	/**
	 * Delete the file, or whole folder.
	 *
	 * @return array|WP_Error
	 */
	public function delete() {
		$data = $this->owner->raw_data;
		$scan = Scan::get_last();
		$path = $data['file'];
		if ( ! file_exists( $path ) || ! is_readable( $path ) ) {
			$scan->remove_issue( $this->owner->id );
			$this->log(
				sprintf( '%s is not readable and will be removed from the scan result', $path ),
				\WP_Defender\Controller\Scan::SCAN_LOG
			);

			return array( 'message' => esc_html__( 'This item has been deleted.', 'wpdef' ) );
		}
		/**
		 * If the path inside theme or plugin folder, then we remove the whole plugin/theme for prevent fatal error.
		 * If the path is to wp-config, we can't delete it.
		 */
		if ( defender_wp_config_path() === $path ) {
			return new WP_Error(
				Error_Code::INVALID,
				esc_html__( 'wp-config.php can\'t be removed. Please remove the suspicious code manually.', 'wpdef' )
			);
		}
		// If it's not relative path, which mean it is something lay on wp-content folder, we can delete it.
		if ( ! $this->delete_infected_file( $path ) ) {
			return $this->get_permission_error( $path );
		}

		return $this->after_delete( $path, $scan, 'malware' );
	}

	/**
	 *  Return the source code depending on the type of the issue.
	 *
	 * @return void|array
	 */
	public function pull_src() {
		global $wp_filesystem;
		// Initialize the WP filesystem, no more using 'file-put-contents' function.
		if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) {
			require_once ABSPATH . '/wp-admin/includes/file.php';
			WP_Filesystem();
		}
		$data = $this->owner->raw_data;

		$file_path = wp_normalize_path( $data['file'] );

		$active   = false;
		$type_src = '';
		if ( ! file_exists( $file_path ) ) {
			return array(
				'origin' => '',
				'code'   => '',
			);
		} elseif ( false !== stripos( $file_path, wp_normalize_path( WP_PLUGIN_DIR ) ) ) {
			$active   = $this->is_active_plugin( $file_path );
			$type_src = 'plugin';
		} elseif ( false !== stripos( $file_path, wp_normalize_path( $this->get_path_of_themes_dir() ) ) ) {
			$active   = $this->is_active_theme( $file_path );
			$type_src = 'theme';
		}

		$markers = array();
		foreach ( $data as $item ) {
			if ( isset( $item['catches'] ) ) {
				foreach ( $item['catches'] as $catch ) {
					$markers = array_merge( $markers, $catch['mapper'] );
				}
			}
		}

		$success_data = array(
			'origin'   => $wp_filesystem->get_contents( $file_path ),
			'mapper'   => $markers,
			'active'   => $active,
			'type_src' => $type_src,
		);

		if ( 'plugin' === $type_src ) {
			$directory_name = $this->get_plugin_directory_name( $file_path );
			$plugin_headers = $this->get_plugin_headers( $file_path );
			if ( empty( $plugin_headers ) ) {
				return $success_data;
			}

			$plugin_headers = reset( $plugin_headers );

			$success_data['local_file_state'] = $this->get_local_file_state(
				$file_path,
				$directory_name,
				$plugin_headers
			);

			$success_data['directory_name'] = $directory_name;
			$success_data['author']         = $plugin_headers['Author'] ?? '';
			$success_data['author_uri']     = $plugin_headers['AuthorURI'] ?? '';
		}

		wp_send_json_success( $success_data );
	}

	/**
	 * Get local file state which used to handle repair or delete in front end.
	 *
	 * @param  string $file_path  File full path.
	 * @param  string $directory_name  Plugin directory name.
	 * @param  array  $plugin_headers  Plugin headers.
	 *
	 * @return string Get local file state.
	 */
	private function get_local_file_state( $file_path, $directory_name, $plugin_headers ): string {
		$file_relative_path = $this->get_plugin_relative_path( $file_path );

		$source_file_url = $this->get_file_url(
			$directory_name,
			$plugin_headers['Version'],
			$file_relative_path
		);

		/**
		 * Retrieves an instance of the File_Helper class.
		 *
		 * @var File_Helper $file_helper
		 */
		$file_helper = wd_di()->get( File_Helper::class );

		$is_identical = $file_helper->is_identical_content(
			$file_path,
			$source_file_url
		);

		if (
			is_wp_error( $is_identical ) &&
			'http_404' === $is_identical->get_error_code()
		) {
			// If file is not exists in remote then it may not be repaired.
			$local_file_state = self::REMOTE_FILE_NOT_EXISTS;
		} elseif ( false === $is_identical ) {
			$local_file_state = self::REMOTE_FILE_DIFFER;
		} else {
			$local_file_state = self::REMOTE_FILE_IDENTICAL;
		}

		return $local_file_state;
	}

	/**
	 * A wrapper method for WP_Defender\Behavior\Scan_Item\Plugin_Integrity::resolve().
	 */
	public function resolve() {
		$file_path = $this->owner->raw_data['file'];

		$directory_name = $this->get_plugin_directory_name( $file_path );
		$plugin_headers = $this->get_plugin_headers( $file_path );
		$plugin_headers = reset( $plugin_headers );

		$local_file_state = $this->get_local_file_state(
			$file_path,
			$directory_name,
			$plugin_headers
		);

		if ( 'REMOTE_FILE_DIFFER' !== $local_file_state ) {
			return;
		}

		/**
		 * Retrieves an instance of the Plugin_Integrity class.
		 *
		 * @var Plugin_Integrity $plugin_integrity
		 */
		$plugin_integrity = wd_di()->get( Plugin_Integrity::class );

		$this->owner->raw_data['type'] = 'modified';

		$plugin_integrity->owner = $this->owner;

		return $plugin_integrity->resolve();
	}
}