<?php

namespace Automattic\WooCommerce\Blueprint\Importers;

use Automattic\WooCommerce\Blueprint\StepProcessor;
use Automattic\WooCommerce\Blueprint\StepProcessorResult;
use Automattic\WooCommerce\Blueprint\Steps\RunSql;
use Automattic\WooCommerce\Blueprint\UsePluginHelpers;
use Automattic\WooCommerce\Blueprint\UseWPFunctions;

/**
 * Processes SQL execution steps in the Blueprint.
 *
 * Handles the execution of SQL queries with safety checks to prevent
 * unauthorized modifications to sensitive WordPress data.
 *
 * @package Automattic\WooCommerce\Blueprint\Importers
 */
class ImportRunSql implements StepProcessor {
	use UsePluginHelpers;
	use UseWPFunctions;

	/**
	 * List of allowed SQL query types.
	 *
	 * @var array
	 */
	private const ALLOWED_QUERY_TYPES = array(
		'INSERT',
		'UPDATE',
		'REPLACE INTO',
	);


	/**
	 * Process the SQL execution step.
	 *
	 * Validates and executes the SQL query while ensuring:
	 * 1. Only allowed query types are executed
	 * 2. No modifications to admin users or roles
	 * 3. No unauthorized changes to user capabilities
	 *
	 * @param object $schema The schema containing the SQL query to execute.
	 * @return StepProcessorResult The result of the SQL execution.
	 */
	public function process( $schema ): StepProcessorResult {
		global $wpdb;
		$result = StepProcessorResult::success( RunSql::get_step_name() );

		$sql = trim( $schema->sql->contents );
		$sql = str_replace( RunSql::TABLE_PREFIX_PLACEHOLDER, $wpdb->prefix, $sql );

		// Check if the query type is allowed.
		if ( ! $this->is_allowed_query_type( $sql ) ) {
			$result->add_error(
				sprintf(
					'Only %s queries are allowed.',
					implode( ', ', self::ALLOWED_QUERY_TYPES )
				)
			);
			return $result;
		}

		// Check for SQL comments that might be hiding malicious code.
		if ( $this->contains_suspicious_comments( $sql ) ) {
			$result->add_error( 'SQL query contains suspicious comment patterns.' );
			return $result;
		}

		// Detect SQL injection patterns.
		if ( $this->contains_sql_injection_patterns( $sql ) ) {
			$result->add_error( 'SQL query contains potential injection patterns.' );
			return $result;
		}

		// Check if the query affects protected tables.
		if ( $this->affects_protected_tables( $sql ) ) {
			$result->add_error( 'Modifications to admin users or roles are not allowed.' );
			return $result;
		}

		// Check if the query affects user capabilities in wp_options.
		if ( $this->affects_user_capabilities( $sql ) ) {
			$result->add_error( 'Modifications to user roles or capabilities are not allowed.' );
			return $result;
		}

		$wpdb->suppress_errors( true );
		$wpdb->query( 'START TRANSACTION' );

		try {
			$query_result = $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared

			$last_error = $wpdb->last_error;
			if ( $last_error ) {
				$wpdb->query( 'ROLLBACK' );
				$result->add_error( 'Error executing SQL: ' . $last_error );
			} else {
				$wpdb->query( 'COMMIT' );
				$result->add_debug( "Executed SQL ({$schema->sql->name}): Affected {$query_result} rows" );
			}
		} catch ( \Throwable $e ) {
			$wpdb->query( 'ROLLBACK' );
			$result->add_error( "Exception executing SQL: {$e->getMessage()}" );
		}

		return $result;
	}

	/**
	 * Returns the class name of the step this processor handles.
	 *
	 * @return string The class name of the step this processor handles.
	 */
	public function get_step_class(): string {
		return RunSql::class;
	}

	/**
	 * Check if the current user has the required capabilities for this step.
	 *
	 * @param object $schema The schema to process.
	 *
	 * @return bool True if the user has the required capabilities. False otherwise.
	 */
	public function check_step_capabilities( $schema ): bool {
		if ( ! current_user_can( 'manage_options' ) ) {
			return false;
		}

		if ( ! current_user_can( 'edit_posts' ) ) {
			return false;
		}

		if ( ! current_user_can( 'edit_users' ) ) {
			return false;
		}

		return true;
	}
	/**
	 * Check if the SQL query type is allowed.
	 *
	 * @param string $sql_content The SQL query to check.
	 * @return bool True if the query type is allowed, false otherwise.
	 */
	private function is_allowed_query_type( string $sql_content ): bool {
		$uppercase_sql_content = strtoupper( trim( $sql_content ) );

		foreach ( self::ALLOWED_QUERY_TYPES as $query_type ) {
			if ( 0 === stripos( $uppercase_sql_content, $query_type ) ) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Check for suspicious comment patterns that might hide malicious code.
	 *
	 * This method detects various types of SQL comments that might be used
	 * to hide malicious SQL commands or bypass security filters.
	 *
	 * @param string $sql_content The SQL query to check.
	 * @return bool True if suspicious comments found, false otherwise.
	 */
	private function contains_suspicious_comments( string $sql_content ): bool {
		// Quick check if there are any comments at all before running regex.
		if (
			strpos( $sql_content, '--' ) === false &&
			strpos( $sql_content, '/*' ) === false &&
			strpos( $sql_content, '#' ) === false
		) {
			return false;
		}

		// List of potentially dangerous SQL commands to check for in comments.
		$dangerous_commands = array(
			'DELETE',
			'DROP',
			'ALTER',
			'CREATE',
			'TRUNCATE',
			'GRANT',
			'REVOKE',
			'EXEC',
			'EXECUTE',
			'CALL',
			'INTO OUTFILE',
			'INTO DUMPFILE',
			'LOAD_FILE',
			'LOAD DATA',
			'BENCHMARK',
			'SLEEP',
			'INFORMATION_SCHEMA',
			'USER\\(',
			'DATABASE\\(',
			'SCHEMA\\(',
		);

		$dangerous_pattern = implode( '|', $dangerous_commands );

		// Check for SQL comments that might be hiding malicious code.
		$patterns = array(
			// Single-line comments (-- style) containing dangerous commands.
			'/--.*?(' . $dangerous_pattern . ')/i',
			// Single-line comments (# style) containing dangerous commands.
			'/#.*?(' . $dangerous_pattern . ')/i',
			// Multi-line comments hiding dangerous commands.
			'/\/\*.*?(' . $dangerous_pattern . ').*?\*\//is',
			// MySQL-specific execution comments (version-specific code execution).
			'/\/\*![0-9]*.*?\*\//',
		);

		foreach ( $patterns as $pattern ) {
			if ( preg_match( $pattern, $sql_content ) ) {
				return true;
			}
		}
		return false;
	}


	/**
	 * Check for common SQL injection patterns.
	 *
	 * @param string $sql_content The SQL query to check.
	 * @return bool True if potential injection patterns found, false otherwise.
	 */
	private function contains_sql_injection_patterns( string $sql_content ): bool {
		$patterns = array(
			'/UNION\s+(?:ALL\s+)?SELECT/i',  // UNION-based injections.
			'/OR\s+1\s*=\s*1/i',             // OR 1=1 condition.
			'/AND\s+0\s*=\s*0/i',            // AND 0=0 condition.
			'/;\s*--/i',                     // Inline comment terminations.
			'/SLEEP\s*\(/i',                 // Time-based injections.
			'/BENCHMARK\s*\(/i',             // Benchmark-based injections.
			'/LOAD_FILE\s*\(/i',             // File access.
			'/INTO\s+OUTFILE/i',             // File write.
			'/INTO\s+DUMPFILE/i',            // File dump.
			'/CREATE\s+(?:TEMPORARY\s+)?TABLE/i',  // Table creation.
			'/DROP\s+TABLE/i',               // Table deletion.
			'/ALTER\s+TABLE/i',              // Table alteration.
			'/INFORMATION_SCHEMA/i',         // Database metadata access.
			'/EXEC\s*\(/i',                  // Stored procedure execution.
			'/SCHEMA_NAME/i',                // Schema access.
			'/DATABASE\(\)/i',               // Current database name.
			'/CHR\s*\(/i',                   // Character function for evasion.
			'/CHAR\s*\(/i',                  // Character function for evasion.
			'/FROM\s+mysql\./i',             // Direct MySQL system table access.
			'/FROM\s+information_schema\./i', // Direct information schema access.
		);
		foreach ( $patterns as $pattern ) {
			if ( preg_match( $pattern, $sql_content ) ) {
				return true;
			}
		}

		return false;
	}


	/**
	 * Check if the SQL query affects protected user tables.
	 *
	 * @param string $sql_content The SQL query to check.
	 * @return bool True if the query affects protected tables, false otherwise.
	 */
	private function affects_protected_tables( string $sql_content ): bool {
		global $wpdb;
		$protected_tables = array(
			$wpdb->users,
			$wpdb->usermeta,
		);

		foreach ( $protected_tables as $table ) {
			if ( preg_match( '/\b' . preg_quote( $table, '/' ) . '\b/i', $sql_content ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Check if the SQL query affects user capabilities in wp_options.
	 *
	 * @param string $sql_content The SQL query to check.
	 * @return bool True if the query affects user capabilities, false otherwise.
	 */
	private function affects_user_capabilities( string $sql_content ): bool {
		global $wpdb;

		// Check if the query affects user capabilities in wp_options.
		if ( stripos( $sql_content, $wpdb->prefix . 'options' ) !== false ) {
			$option_patterns = array(
				'user_roles',
				'capabilities',
				'wp_user_',
				'role_',
				'administrator',
			);

			foreach ( $option_patterns as $pattern ) {
				if ( stripos( $sql_content, $pattern ) !== false ) {
					return true;
				}
			}
		}
		return false;
	}
}
