<?php
/**
 * Starter Templates WXR Importer - Module.
 *
 * This file is used to register and manage the Zip AI Modules.
 *
 * @package Starter Templates Importer
 */

namespace STImporter\Importer\WXR_Importer;

use STImporter\Importer\ST_Importer_Helper;
use STImporter\Importer\ST_Importer_Log;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}


/**
 * The Module Class.
 */
class ST_WXR_Importer {

	/**
	 * Instance of this class.
	 *
	 * @since 1.0.0
	 * @var object Class object.
	 */
	private static $instance = null;

	/**
	 * Transient key for WXR import progress.
	 *
	 * @since 1.1.24
	 * @var string
	 */
	private $wxr_import_progress_key = 'st_wxr_importer_progress';

	/**
	 * Set of post IDs present in the WXR file.
	 * Used to detect orphaned attachments whose post_parent has no matching item.
	 *
	 * @since 1.1.31
	 * @var array<int, true>
	 */
	private $wxr_post_ids = array();

	/**
	 * Initiator of this class.
	 *
	 * @since 1.0.0
	 * @return self initialized object of this class.
	 */
	public static function get_instance() {
		if ( null === self::$instance ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	/**
	 * Constructor
	 *
	 * @since 1.0.0
	 */
	public function __construct() {
		add_filter( 'upload_mimes', array( $this, 'custom_upload_mimes' ) ); //phpcs:ignore WordPressVIPMinimum.Hooks.RestrictedHooks.upload_mimes -- Added this to allow upload of SVG files.
		add_action( 'wp_ajax_astra-wxr-import', array( $this, 'sse_import' ) );
		add_filter( 'wxr_importer.pre_process.user', '__return_null' );
		add_filter( 'wp_import_post_data_processed', array( $this, 'pre_post_data' ), 10, 2 );
		add_filter( 'wxr_importer.pre_process.post', array( $this, 'pre_process_post' ), 10, 4 );
		if ( version_compare( get_bloginfo( 'version' ), '5.1.0', '>=' ) ) {
			add_filter( 'wp_check_filetype_and_ext', array( $this, 'real_mime_types_5_1_0' ), 10, 5 );
		} else {
			add_filter( 'wp_check_filetype_and_ext', array( $this, 'real_mime_types' ), 10, 4 );
		}
		add_action( 'wp_import_insert_post', array( $this, 'after_imported_post' ), 10, 4 );

		// To handle the multiple WXR import requests.
		add_action( 'import_start', array( $this, 'wxr_import_transient_start' ) );
		add_action( 'import_end', array( $this, 'wxr_import_transient_cleanup' ) );

		// SureDonation auto-creates a default form when a campaign is published.
		// The imported WXR already carries the campaign's real form, so suppress
		// the auto-creation during import and remap the campaign/form
		// cross-reference metas once all posts are in.
		add_action( 'import_start', array( $this, 'suppress_suredonation_auto_form' ) );
		add_action( 'import_end', array( $this, 'restore_suredonation_auto_form' ) );
		add_action( 'import_end', array( $this, 'remap_suredonation_relations' ) );

		// SureMembers access-group rules reference source-site post and term IDs.
		// Track term imports and remap the rule metas once all content is in.
		add_action( 'wxr_importer.processed.term', array( $this, 'track_suremembers_term' ), 10, 2 );
		add_action( 'import_end', array( $this, 'remap_suremembers_relations' ) );
	}

	/**
	 * Detach SureDonation's default-form auto-creation during WXR import.
	 *
	 * Publishing a `suredonation_cmpgn` post normally triggers
	 * `Campaign_Cpt::maybe_create_default_form()`, which would generate a second
	 * donation form alongside the one carried by the WXR file (the campaign's
	 * `_suredonation_default_form_id` meta is not yet inserted when `save_post`
	 * fires mid-import, so its own guard cannot help).
	 *
	 * @since 1.1.35
	 * @return void
	 */
	public function suppress_suredonation_auto_form() {
		if ( ! class_exists( 'SureDonation\Inc\Campaigns\Campaign_Cpt' ) ) {
			return;
		}

		remove_action( 'save_post_suredonation_cmpgn', array( \SureDonation\Inc\Campaigns\Campaign_Cpt::get_instance(), 'maybe_create_default_form' ), 20 );
	}

	/**
	 * Re-attach SureDonation's default-form auto-creation after WXR import.
	 *
	 * @since 1.1.35
	 * @return void
	 */
	public function restore_suredonation_auto_form() {
		if ( ! class_exists( 'SureDonation\Inc\Campaigns\Campaign_Cpt' ) ) {
			return;
		}

		add_action( 'save_post_suredonation_cmpgn', array( \SureDonation\Inc\Campaigns\Campaign_Cpt::get_instance(), 'maybe_create_default_form' ), 20, 2 );
	}

	/**
	 * Remap SureDonation campaign/form cross-reference metas to imported IDs.
	 *
	 * The campaign stores its default form in `_suredonation_default_form_id`
	 * and the form stores its campaign in `_suredonation_campaign_id` — both as
	 * source-site post IDs. Runs on `import_end`, when the campaign and form ID
	 * maps captured during the WXR import are complete.
	 *
	 * @since 1.1.35
	 * @return void
	 */
	public function remap_suredonation_relations() {
		$campaign_id_map = get_option( 'astra_sites_suredonation_campaign_id_map', array() );
		$form_id_map     = get_option( 'astra_sites_suredonation_form_id_map', array() );

		$campaign_id_map = is_array( $campaign_id_map ) ? $campaign_id_map : array();
		$form_id_map     = is_array( $form_id_map ) ? $form_id_map : array();

		// Campaign meta -> new form ID.
		foreach ( $campaign_id_map as $new_campaign_id ) {
			$old_form_id = (int) get_post_meta( $new_campaign_id, '_suredonation_default_form_id', true );
			if ( $old_form_id && isset( $form_id_map[ $old_form_id ] ) ) {
				update_post_meta( $new_campaign_id, '_suredonation_default_form_id', (int) $form_id_map[ $old_form_id ] );
			}
		}

		// Form meta -> new campaign ID.
		foreach ( $form_id_map as $new_form_id ) {
			$old_campaign_id = (int) get_post_meta( $new_form_id, '_suredonation_campaign_id', true );
			if ( $old_campaign_id && isset( $campaign_id_map[ $old_campaign_id ] ) ) {
				update_post_meta( $new_form_id, '_suredonation_campaign_id', (int) $campaign_id_map[ $old_campaign_id ] );
			}
		}
	}

	/**
	 * After Post import action.
	 *
	 * @param int                   $post_id post id.
	 * @param int                   $original_id post id.
	 * @param array<string, string> $postdata post id.
	 * @param array<string, string> $data post id.
	 *
	 * @return void
	 */
	public function after_imported_post( $post_id, $original_id, $postdata, $data ) {
		// Log individual post import.
		ST_Importer_Log::add(
			'success',
			sprintf(
				'Post imported successfully: ID %d, Type: %s, Original ID: %d',
				$post_id,
				$data['post_type'],
				$original_id
			),
			array(
				'post_id'     => $post_id,
				'post_type'   => $data['post_type'],
				'original_id' => $original_id,
				'post_title'  => isset( $data['post_title'] ) ? $data['post_title'] : '',
			)
		);

		if ( in_array( $data['post_type'], array( 'post', 'page' ), true ) && 'ai' === get_option( 'astra_sites_current_import_template_type' ) ) {
			$imports                         = get_option(
				'astra_sites_ai_imports',
				array(
					'post' => array(),
					'page' => array(),
				)
			);
			$imports[ $data['post_type'] ][] = $post_id;
			update_option( 'astra_sites_ai_imports', $imports );
		}

		if ( 'sureforms_form' === get_post_type( $post_id ) ) {
			$sureforms_id_map                 = get_option( 'astra_sites_sureforms_id_map', array() );
			$sureforms_id_map[ $original_id ] = $post_id;
			update_option( 'astra_sites_sureforms_id_map', $sureforms_id_map );
		}

		if ( 'sc_form' === get_post_type( $post_id ) ) {
			$sureforms_id_map                 = get_option( 'astra_sites_surecart_forms_id_map', array() );
			$sureforms_id_map[ $original_id ] = $post_id;
			update_option( 'astra_sites_surecart_forms_id_map', $sureforms_id_map );
		}

		if ( 'suredonation_cmpgn' === get_post_type( $post_id ) ) {
			$suredonation_campaign_id_map                 = get_option( 'astra_sites_suredonation_campaign_id_map', array() );
			$suredonation_campaign_id_map[ $original_id ] = $post_id;
			update_option( 'astra_sites_suredonation_campaign_id_map', $suredonation_campaign_id_map );
		}

		if ( 'suredonation_form' === get_post_type( $post_id ) ) {
			$suredonation_form_id_map                 = get_option( 'astra_sites_suredonation_form_id_map', array() );
			$suredonation_form_id_map[ $original_id ] = $post_id;
			update_option( 'astra_sites_suredonation_form_id_map', $suredonation_form_id_map );
		}

		if ( defined( 'SUREMEMBERS_POST_TYPE' ) && ! empty( $original_id ) ) {
			$post_type = get_post_type( $post_id );

			if ( SUREMEMBERS_POST_TYPE === $post_type ) {
				$access_group_id_map                 = get_option( 'astra_sites_suremembers_access_group_id_map', array() );
				$access_group_id_map[ $original_id ] = $post_id;
				update_option( 'astra_sites_suremembers_access_group_id_map', $access_group_id_map, 'no' );
			} elseif ( 'attachment' !== $post_type ) {
				// Access-group rules can target any post type ("post-{id}-|" /
				// "postchild-{id}-|" rule strings), so track every content post.
				$post_id_map                 = get_option( 'astra_sites_suremembers_post_id_map', array() );
				$post_id_map[ $original_id ] = $post_id;
				update_option( 'astra_sites_suremembers_post_id_map', $post_id_map, 'no' );
			}
		}
	}

	/**
	 * Track imported terms for the SureMembers rule remap.
	 *
	 * Access-group rules can target taxonomy terms via "tax-{term_id}-single-{taxonomy}"
	 * rule strings, so keep an old → new term ID map alongside the post map.
	 *
	 * @since 1.1.37
	 *
	 * @param int   $term_id New term ID.
	 * @param array $data    Raw data imported for the term.
	 * @return void
	 */
	public function track_suremembers_term( $term_id, $data = array() ) {
		if ( ! defined( 'SUREMEMBERS_POST_TYPE' ) ) {
			return;
		}

		$original_id = isset( $data['id'] ) ? absint( $data['id'] ) : 0;

		if ( empty( $original_id ) || empty( $term_id ) ) {
			return;
		}

		$term_id_map                 = get_option( 'astra_sites_suremembers_term_id_map', array() );
		$term_id_map[ $original_id ] = (int) $term_id;
		update_option( 'astra_sites_suremembers_term_id_map', $term_id_map, 'no' );
	}

	/**
	 * Remap SureMembers access-group relations to the imported IDs.
	 *
	 * Two directions need fixing once the import completes:
	 * 1. Rule metas on each imported access group (include/exclude/drips/rules)
	 *    hold rule strings such as "post-{id}-|", "postchild-{id}-|" and
	 *    "tax-{term_id}-single-{taxonomy}" that still carry source-site IDs.
	 * 2. Restricted posts carry the source access-group IDs in their
	 *    "suremembers_post_access_group" meta.
	 *
	 * Runs on `import_end`, when the post/term/group ID maps captured during
	 * the WXR import are complete. Each object is remapped only once (guarded
	 * by a marker meta) so a resumed import cannot remap an already-new ID.
	 *
	 * @since 1.1.37
	 * @return void
	 */
	public function remap_suremembers_relations() {
		$access_group_id_map = get_option( 'astra_sites_suremembers_access_group_id_map', array() );

		if ( empty( $access_group_id_map ) || ! is_array( $access_group_id_map ) ) {
			return;
		}

		$post_id_map = get_option( 'astra_sites_suremembers_post_id_map', array() );
		$term_id_map = get_option( 'astra_sites_suremembers_term_id_map', array() );

		$post_id_map = is_array( $post_id_map ) ? $post_id_map : array();
		$term_id_map = is_array( $term_id_map ) ? $term_id_map : array();

		$rule_meta_keys = array(
			'suremembers_plan_include',
			'suremembers_plan_exclude',
			'suremembers_plan_drips',
		);

		foreach ( $access_group_id_map as $new_group_id ) {
			$new_group_id = (int) $new_group_id;

			if ( get_post_meta( $new_group_id, '_astra_sites_suremembers_remapped', true ) ) {
				continue;
			}

			foreach ( $rule_meta_keys as $meta_key ) {
				$meta_value = get_post_meta( $new_group_id, $meta_key, true );

				if ( empty( $meta_value ) ) {
					continue;
				}

				$remapped = $this->remap_suremembers_rule_value( $meta_value, $post_id_map, $term_id_map );

				if ( $remapped !== $meta_value ) {
					update_post_meta( $new_group_id, $meta_key, $remapped );
				}
			}

			update_post_meta( $new_group_id, '_astra_sites_suremembers_remapped', true );
		}

		$this->remap_suremembers_post_access_groups( $access_group_id_map );
	}

	/**
	 * Recursively remap source-site IDs inside SureMembers rule meta values.
	 *
	 * Rule strings live as individual array entries, so the patterns are
	 * anchored — any other string (URLs, preview content, labels) is left
	 * untouched. Handled formats: "post-{id}-|", "postchild-{id}-|" and
	 * "tax-{term_id}-single-{taxonomy}".
	 *
	 * @since 1.1.37
	 *
	 * @param mixed                  $value       Meta value (array or scalar).
	 * @param array<int|string, int> $post_id_map Old → new post IDs.
	 * @param array<int|string, int> $term_id_map Old → new term IDs.
	 * @return mixed
	 */
	public function remap_suremembers_rule_value( $value, $post_id_map, $term_id_map ) {
		if ( is_array( $value ) ) {
			foreach ( $value as $key => $item ) {
				$value[ $key ] = $this->remap_suremembers_rule_value( $item, $post_id_map, $term_id_map );
			}
			return $value;
		}

		if ( ! is_string( $value ) ) {
			return $value;
		}

		$value = preg_replace_callback(
			'/^(post|postchild)-(\d+)-\|$/',
			function ( $matches ) use ( $post_id_map ) {
				$old_id = (int) $matches[2];
				$new_id = isset( $post_id_map[ $old_id ] ) ? (int) $post_id_map[ $old_id ] : $old_id;
				return $matches[1] . '-' . $new_id . '-|';
			},
			$value
		);

		$remapped = preg_replace_callback(
			'/^tax-(\d+)-single-([a-zA-Z0-9_\-]+)$/',
			function ( $matches ) use ( $term_id_map ) {
				$old_id = (int) $matches[1];
				$new_id = isset( $term_id_map[ $old_id ] ) ? (int) $term_id_map[ $old_id ] : $old_id;
				return 'tax-' . $new_id . '-single-' . $matches[2];
			},
			$value
		);

		return null !== $remapped ? $remapped : $value;
	}

	/**
	 * Remap the "suremembers_post_access_group" meta on imported posts.
	 *
	 * Restricted posts arrive from the WXR with the source-site access-group
	 * IDs; point them at the newly imported access groups.
	 *
	 * @since 1.1.37
	 *
	 * @param array<int|string, int> $access_group_id_map Old → new access group IDs.
	 * @return void
	 */
	public function remap_suremembers_post_access_groups( $access_group_id_map ) {
		$post_ids = get_posts(
			array(
				'post_type'      => 'any',
				'post_status'    => 'any',
				'posts_per_page' => -1,
				'fields'         => 'ids',
				'meta_query'     => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- One-time remap at the end of the import.
					array(
						'key'     => 'suremembers_post_access_group',
						'compare' => 'EXISTS',
					),
					array(
						'key'     => '_astra_sites_suremembers_groups_remapped',
						'compare' => 'NOT EXISTS',
					),
					array(
						'key'     => '_astra_sites_imported_post',
						'compare' => 'EXISTS',
					),
				),
			)
		);

		foreach ( $post_ids as $post_id ) {
			$groups = get_post_meta( (int) $post_id, 'suremembers_post_access_group', true );

			if ( is_array( $groups ) && ! empty( $groups ) ) {
				$remapped = array();
				$changed  = false;

				foreach ( $groups as $group_id ) {
					$group_id   = (int) $group_id;
					$new_id     = isset( $access_group_id_map[ $group_id ] ) ? (int) $access_group_id_map[ $group_id ] : $group_id;
					$changed    = $changed || $new_id !== $group_id;
					$remapped[] = $new_id;
				}

				if ( $changed ) {
					update_post_meta( (int) $post_id, 'suremembers_post_access_group', $remapped );
				}
			}

			update_post_meta( (int) $post_id, '_astra_sites_suremembers_groups_remapped', true );
		}
	}

	/**
	 * Add .xml files as supported format in the uploader.
	 *
	 * @since 1.1.5 Added SVG file support.
	 *
	 * @since 1.0.0
	 *
	 * @param array $mimes Already supported mime types.
	 */
	public function custom_upload_mimes( $mimes ) {

		// Allow SVG files.
		$mimes['svg']  = 'image/svg+xml';
		$mimes['svgz'] = 'image/svg+xml';

		// Allow XML files.
		$mimes['xml'] = 'text/xml';

		// Allow JSON files.
		$mimes['json'] = 'application/json';

		return $mimes;
	}

	/**
	 * WXR Import Transient Start
	 *
	 * @since 1.1.24
	 */
	public function wxr_import_transient_start() {
		set_transient( $this->wxr_import_progress_key, 'ongoing', 300 ); // 5 minutes.
	}

	/**
	 * WXR Import Transient Cleanup
	 *
	 * @since 1.1.24
	 */
	public function wxr_import_transient_cleanup() {
		set_transient( $this->wxr_import_progress_key, 'completed', 30 ); // 30 seconds.
	}

	/**
	 * Is WXR Import In Progress
	 *
	 * @since 1.1.24
	 * @return bool True if in progress, false otherwise.
	 */
	public function is_wxr_import_in_progress() {
		// Check existing progress.
		$wxr_progress = get_transient( $this->wxr_import_progress_key );
		if ( ! $wxr_progress ) {
			return false;
		}

		if ( 'completed' === $wxr_progress ) {
			$data = array(
				'action' => 'complete',
				'error'  => false,
			);
		} else {
			$data = array(
				'action' => 'updatedDelta',
				'type'   => 'status',
				'delta'  => 1,
			);
		}

		$this->emit_sse_message( $data );
		return true;
	}

	/**
	 * Constructor.
	 *
	 * @since  1.1.0
	 * @since  1.4.0 The `$xml_url` was added.
	 *
	 * @param  string $xml_url XML file URL.
	 */
	public function sse_import( $xml_url = '' ) {
		// Log import start.
		ST_Importer_Log::add(
			'info',
			'WXR import process started',
			array(
				'xml_url_provided' => ! empty( $xml_url ),
			)
		);

		if ( wp_doing_ajax() ) {

			// Verify Nonce.
			check_ajax_referer( 'astra-sites', '_ajax_nonce' );

			if ( ! current_user_can( 'manage_options' ) ) {
				// Log permission error.
				ST_Importer_Log::add(
					'error',
					'Permission denied: User lacks manage_options capability',
					array(
						'user_id'   => get_current_user_id(),
						'user_caps' => current_user_can( 'manage_options' ) ? 'true' : 'false',
					)
				);

				wp_send_json_error(
					array(
						'error' => __( "Permission denied: You don't have sufficient permissions to perform this action. Please contact your site administrator.", 'astra-sites' ),
					)
				);
			}

			// Start the event stream.
			header( 'Content-Type: text/event-stream, charset=UTF-8' );
			header( 'Cache-Control: no-cache' );
			header( 'Connection: keep-alive' );
			// Turn off PHP output compression.
			$previous = error_reporting( error_reporting() ^ E_WARNING ); //phpcs:ignore WordPress.PHP.DevelopmentFunctions, WordPress.PHP.DiscouragedPHPFunctions -- 3rd party library.
			ini_set( 'output_buffering', 'off' ); //phpcs:ignore WordPress.PHP.IniSet.Risky, Generic.PHP.ForbiddenFunctions.FoundWithAlternative -- 3rd party library.
			ini_set( 'zlib.output_compression', false ); //phpcs:ignore WordPress.PHP.IniSet.Risky, Generic.PHP.ForbiddenFunctions.FoundWithAlternative -- 3rd party library.
			error_reporting( $previous ); //phpcs:ignore WordPress.PHP.DevelopmentFunctions, WordPress.PHP.DiscouragedPHPFunctions -- 3rd party library.

			if ( $GLOBALS['is_nginx'] ) {
				// Setting this header instructs Nginx to disable fastcgi_buffering
				// and disable gzip for this request.
				header( 'X-Accel-Buffering: no' );
				header( 'Content-Encoding: none' );
			}

			// 2KB padding for IE.
			echo esc_html( ':' . str_repeat( ' ', 2048 ) . "\n\n" );
		}

		$xml_id = isset( $_REQUEST['xml_id'] ) ? absint( $_REQUEST['xml_id'] ) : '';
		if ( ! empty( $xml_id ) ) {
			$xml_url = get_attached_file( $xml_id );
		}

		// Check for existing progress to prevent duplicate content.
		if ( $this->is_wxr_import_in_progress() ) {
			return;
		}

		// Enhanced XML file validation.
		if ( empty( $xml_url ) ) {
			// Log validation error - empty XML URL.
			ST_Importer_Log::add(
				'fatal',
				'XML file URL is empty or not provided',
				array(
					'xml_url' => $xml_url,
					'xml_id'  => isset( $_REQUEST['xml_id'] ) ? absint( $_REQUEST['xml_id'] ) : '',
				)
			);

			$this->wxr_import_transient_cleanup();
			$this->emit_sse_message(
				array(
					'action' => 'complete',
					'error'  => __( 'Template content file not found or inaccessible. The import process cannot continue without the content file. Please try importing again.', 'astra-sites' ),
				)
			);
			if ( wp_doing_ajax() ) {
				exit;
			}
			return;
		}

		if ( ! file_exists( $xml_url ) ) {
			// Log validation error - file does not exist.
			ST_Importer_Log::add(
				'fatal',
				'XML file does not exist on server',
				array(
					'xml_url' => $xml_url,
				)
			);

			$this->wxr_import_transient_cleanup();
			$this->emit_sse_message(
				array(
					'action' => 'complete',
					'error'  => __( 'Template content file does not exist on the server. The file may have been deleted or moved. Please try re-importing the template.', 'astra-sites' ),
				)
			);
			if ( wp_doing_ajax() ) {
				exit;
			}
			return;
		}

		if ( ! is_readable( $xml_url ) ) {
			// Log validation error - file not readable.
			ST_Importer_Log::add(
				'fatal',
				'XML file is not readable due to file permission issues',
				array(
					'xml_url'    => $xml_url,
					'file_perms' => substr( sprintf( '%o', fileperms( $xml_url ) ), -4 ),
				)
			);

			$this->wxr_import_transient_cleanup();
			$this->emit_sse_message(
				array(
					'action' => 'complete',
					'error'  => __( 'Template content file cannot be read due to file permission issues. Please contact your hosting provider to fix file permissions.', 'astra-sites' ),
				)
			);
			if ( wp_doing_ajax() ) {
				exit;
			}
			return;
		}

		// Time to run the import!
		if ( function_exists( 'set_time_limit' ) ) {
			\set_time_limit( 0 ); // phpcs:ignore Generic.PHP.ForbiddenFunctions.FoundWithAlternative -- Required for long-running import process.
		}

		// Ensure we're not buffered.
		wp_ob_end_flush_all();
		flush();

		do_action( 'astra_sites_before_sse_import' );

		// Enable default GD library.
		add_filter( 'wp_image_editors', array( $this, 'enable_wp_image_editor_gd' ) );

		// Change GUID image URL.
		add_filter( 'wxr_importer.pre_process.post', array( $this, 'fix_image_duplicate_issue' ), 10, 4 );

		// Are we allowed to create users?
		add_filter( 'wxr_importer.pre_process.user', '__return_null' );

		// Keep track of our progress.
		add_action( 'wxr_importer.processed.post', array( $this, 'imported_post' ), 10, 2 );
		add_action( 'wxr_importer.process_failed.post', array( $this, 'imported_post' ), 10, 2 );
		add_action( 'wxr_importer.process_already_imported.post', array( $this, 'already_imported_post' ), 10, 2 );
		add_action( 'wxr_importer.process_skipped.post', array( $this, 'already_imported_post' ), 10, 2 );
		add_action( 'wxr_importer.processed.comment', array( $this, 'imported_comment' ) );
		add_action( 'wxr_importer.process_already_imported.comment', array( $this, 'imported_comment' ) );
		add_action( 'wxr_importer.processed.term', array( $this, 'imported_term' ) );
		add_action( 'wxr_importer.process_failed.term', array( $this, 'imported_term' ) );
		add_action( 'wxr_importer.process_already_imported.term', array( $this, 'imported_term' ) );
		add_action( 'wxr_importer.processed.user', array( $this, 'imported_user' ) );
		add_action( 'wxr_importer.process_failed.user', array( $this, 'imported_user' ) );

		// Keep track of our progress.
		add_action( 'wxr_importer.processed.post', array( $this, 'track_post' ), 10, 2 );
		add_action( 'wxr_importer.processed.term', array( $this, 'track_term' ) );

		// Remove content_save_pr filter to avoid unwanted content filtering. Replicating the same from super admin.
		$has_content_filter = has_filter( 'content_save_pre', 'wp_filter_post_kses' );
		if (
			is_multisite() &&
			current_user_can( 'activate_plugins' ) &&
			$has_content_filter
		) {
			remove_filter( 'content_save_pre', 'wp_filter_post_kses' );
		}

		// Flush once more.
		flush();

		/**
		 * Importer instance.
		 *
		 * @var \WXR_Importer $importer Importer instance.
		 */
		$importer = $this->get_importer();

		// Log import progress started.
		ST_Importer_Log::add(
			'info',
			'Starting WXR file import process',
			array(
				'xml_url'   => $xml_url,
				'file_size' => filesize( $xml_url ),
			)
		);

		// Pre-scan the WXR to collect valid post IDs for orphaned attachment filtering.
		// Skip for AI imports -- their WXR files are dynamically generated and won't have orphans.
		if ( 'ai' !== get_option( 'astra_sites_current_import_template_type' ) ) {
			$this->wxr_post_ids = $this->prescan_wxr_post_ids( $xml_url );
		}

		try {
			$response = $importer->import( $xml_url );
		} catch ( \Exception $e ) {
			// Log exception during import.
			ST_Importer_Log::add(
				'error',
				'Exception caught during WXR import: ' . $e->getMessage(),
				array(
					'exception_message' => $e->getMessage(),
					'exception_code'    => $e->getCode(),
					'xml_url'           => $xml_url,
				)
			);

			$this->wxr_import_transient_cleanup();
			$this->emit_sse_message(
				array(
					'action'    => 'complete',
					'error'     => $this->get_contextual_import_error_message( $e->getMessage() ),
					'technical' => $e->getMessage(),
				)
			);
			if ( wp_doing_ajax() ) {
				exit;
			}
			return;
		} catch ( \Error $e ) {
			// Log fatal error during import.
			ST_Importer_Log::add(
				'fatal',
				'Fatal error occurred during WXR import: ' . $e->getMessage(),
				array(
					'error_message' => $e->getMessage(),
					'error_code'    => $e->getCode(),
					'xml_url'       => $xml_url,
				)
			);

			$this->wxr_import_transient_cleanup();
			$this->emit_sse_message(
				array(
					'action'    => 'complete',
					'error'     => __( 'A fatal error occurred during import, likely due to server resource limitations. Try a different template, or contact your hosting provider to increase server resources.', 'astra-sites' ),
					'technical' => $e->getMessage(),
				)
			);
			if ( wp_doing_ajax() ) {
				exit;
			}
			return;
		}

		// Let the browser know we're done.
		$complete = array(
			'action' => 'complete',
			'error'  => false,
		);
		if ( is_wp_error( $response ) ) {
			// Log WP_Error response.
			ST_Importer_Log::add(
				'error',
				'WP_Error returned from WXR importer: ' . $response->get_error_message(),
				array(
					'error_message' => $response->get_error_message(),
					'error_code'    => $response->get_error_code(),
					'xml_url'       => $xml_url,
				)
			);

			$complete['error']     = $this->get_contextual_import_error_message( $response->get_error_message() );
			$complete['technical'] = $response->get_error_message();
		} else {
			// Log successful import completion.
			ST_Importer_Log::add(
				'success',
				'WXR import completed successfully',
				array(
					'xml_url' => $xml_url,
				)
			);
		}

		// Restore the content filter.
		if ( is_multisite() && $has_content_filter ) {
			add_filter( 'content_save_pre', 'wp_filter_post_kses' );
		}

		$this->emit_sse_message( $complete );
		if ( wp_doing_ajax() ) {
			exit;
		}
	}

	/**
	 * Get contextual error message for import errors
	 *
	 * @since 1.1.21
	 * @param string $original_error Original error message.
	 * @return string Enhanced contextual error message.
	 */
	private function get_contextual_import_error_message( $original_error ) {
		$message_lower = strtolower( $original_error );

		// Memory errors.
		if ( strpos( $message_lower, 'memory' ) !== false ||
			strpos( $message_lower, 'fatal error' ) !== false ) {
			return __( 'Import failed due to server memory limitations. This template may be too large for your current server configuration. Try importing a smaller template, or contact your hosting provider to increase memory limits.', 'astra-sites' );
		}

		// Timeout errors.
		if ( strpos( $message_lower, 'timeout' ) !== false ||
			strpos( $message_lower, 'execution time' ) !== false ) {
			return __( 'Import timed out due to server limitations. The template import process took too long to complete. Try again, or contact your hosting provider to increase execution time limits.', 'astra-sites' );
		}

		// Database errors.
		if ( strpos( $message_lower, 'database' ) !== false ||
			strpos( $message_lower, 'mysql' ) !== false ||
			strpos( $message_lower, 'sql' ) !== false ) {
			return __( 'Import failed due to database issues. This could be due to database connection problems or insufficient database permissions. Please contact your hosting provider.', 'astra-sites' );
		}

		// File/Permission errors.
		if ( strpos( $message_lower, 'permission' ) !== false ||
			strpos( $message_lower, 'write' ) !== false ||
			strpos( $message_lower, 'upload' ) !== false ) {
			return __( 'Import failed due to file permission issues. The server cannot create or write files needed for the import. Please contact your hosting provider to fix file permissions.', 'astra-sites' );
		}

		// Image/Media download errors.
		if ( strpos( $message_lower, 'image' ) !== false ||
			strpos( $message_lower, 'media' ) !== false ||
			strpos( $message_lower, 'download' ) !== false ) {
			return __( 'Import completed but some images or media files could not be downloaded. This could be due to network issues or the original files being unavailable. The template structure has been imported successfully.', 'astra-sites' );
		}

		// XML parsing errors.
		if ( strpos( $message_lower, 'xml' ) !== false ||
			strpos( $message_lower, 'parse' ) !== false ) {
			return __( 'Import failed due to corrupted template content. The template file appears to be damaged or incomplete. Please try importing again, or select a different template.', 'astra-sites' );
		}

		// Default enhanced message.
		return sprintf(
			// translators: Import encountered error text.
			__( 'Import process encountered an error: %s. Please try again, or contact support if the issue persists.', 'astra-sites' ),
			$original_error
		);
	}

	/**
	 * Set GUID as per the attachment URL which avoid duplicate images issue due to the different GUID.
	 *
	 * @param array $data Post data. (Return empty to skip).
	 * @param array $meta Meta data.
	 * @param array $comments Comments on the post.
	 * @param array $terms Terms on the post.
	 */
	public function fix_image_duplicate_issue( $data, $meta, $comments, $terms ) {

		if ( empty( $data ) ) {
			return $data;
		}

		$remote_url   = ! empty( $data['attachment_url'] ) ? $data['attachment_url'] : $data['guid'];
		$data['guid'] = $remote_url;

		return $data;
	}

		/**
		 * Enable the WP_Image_Editor_GD library.
		 *
		 * @since 2.2.3
		 * @param  array $editors Image editors library list.
		 * @return array
		 */
	public function enable_wp_image_editor_gd( $editors ) {
		$gd_editor = 'WP_Image_Editor_GD';
		$editors   = array_diff( $editors, array( $gd_editor ) );
		array_unshift( $editors, $gd_editor );
		return $editors;
	}

	/**
	 * Track Imported Term
	 *
	 * @param  int $term_id Term ID.
	 * @return void
	 */
	public function track_term( $term_id ) {
		$term = get_term( $term_id );
		update_term_meta( $term_id, '_astra_sites_imported_term', true );
	}

	/**
	 * Pre Post Data
	 *
	 * @since 2.1.0
	 *
	 * @param  array $postdata Post data.
	 * @param  array $data     Post data.
	 * @return array           Post data.
	 */
	public function pre_post_data( $postdata, $data ) {

		// Replace source GUID with a deterministic URL-based hash for duplicate detection on retry imports.
		$hash             = md5( $data['post_title'] . '::' . $data['post_type'] . '::' . $data['post_name'] );
		$postdata['guid'] = site_url( '/?st-import=' . $hash );

		return $postdata;
	}

	/**
	 * Pre-scan the WXR file to collect all post IDs present in it.
	 *
	 * Used to detect orphaned attachments whose post_parent references a post
	 * that does not exist in the WXR (e.g. template catalog screenshots that
	 * leaked into the demo site's media library from other templates).
	 *
	 * @since 1.1.31
	 * @param string $file Absolute path to the WXR XML file.
	 * @return array<int, true> Map of post IDs to true for O(1) lookups.
	 */
	private function prescan_wxr_post_ids( $file ) {
		$post_ids = array();

		$reader = new \XMLReader();
		$status = $reader->open( $file, null, LIBXML_NONET );

		if ( ! $status ) {
			return $post_ids;
		}

		while ( $reader->read() ) {
			if ( \XMLReader::ELEMENT !== $reader->nodeType || 'item' !== $reader->name ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- DOMNode property.
				continue;
			}

			$node = $reader->expand();
			if ( false === $node ) {
				$reader->next();
				continue;
			}

			$post_id = 0;

			foreach ( $node->childNodes as $child ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- DOMNode property.
				if ( XML_ELEMENT_NODE !== $child->nodeType ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- DOMNode property.
					continue;
				}
				if ( 'wp:post_id' === $child->tagName ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- DOMNode property.
					$post_id = (int) $child->textContent; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- DOMNode property.
					break;
				}
			}

			if ( $post_id > 0 ) {
				$post_ids[ $post_id ] = true;
			}

			$reader->next();
		}

		$reader->close();

		return $post_ids;
	}

	/**
	 * Pre Process Post
	 *
	 * @since 1.2.12
	 *
	 * @param array $data Post data. (Return empty to skip.).
	 * @param array $meta Meta data.
	 * @param array $comments Comments on the post.
	 * @param array $terms Terms on the post.
	 */
	public function pre_process_post( $data, $meta, $comments, $terms ) {

		// Skip orphaned attachments whose post_parent references a post ID that
		// does not exist in the WXR file. These are template catalog screenshots
		// from other templates that leaked into the demo site's media library.
		if ( ! empty( $this->wxr_post_ids )
			&& isset( $data['post_type'] ) && 'attachment' === $data['post_type']
			&& isset( $data['post_parent'] ) && (int) $data['post_parent'] > 0
			&& ! isset( $this->wxr_post_ids[ (int) $data['post_parent'] ] )
		) {
			return array();
		}

		if ( isset( $data['post_content'] ) ) {

			$meta_data = wp_list_pluck( $meta, 'key' );

			$is_attachment          = ( 'attachment' === $data['post_type'] ) ? true : false;
			$is_elementor_page      = in_array( '_elementor_version', $meta_data, true );
			$is_beaver_builder_page = in_array( '_fl_builder_enabled', $meta_data, true );
			$is_brizy_page          = in_array( 'brizy_post_uid', $meta_data, true );

			$disable_post_content = apply_filters( 'astra_sites_pre_process_post_disable_content', ( $is_attachment || $is_elementor_page || $is_beaver_builder_page || $is_brizy_page ) );

			// If post type is `attachment OR
			// If page contain Elementor, Brizy or Beaver Builder meta then skip this page.
			if ( $disable_post_content ) {
				$data['post_content'] = '';
			} else {
				/**
				 * Gutenberg Content Data Fix
				 *
				 * Gutenberg encode the page content. In import process the encoded characterless e.g. <, > are
				 * decoded into HTML tag and it break the Gutenberg render markup.
				 *
				 * Note: We have not check the post is created with Gutenberg or not. We have imported other sites
				 * and confirm that this works for every other page builders too.
				 */
				if ( 'sureforms_form' !== $data['post_type'] ) {
					$data['post_content'] = wp_slash( $data['post_content'] );
				}
			}
		}

		/**
		 * Setting the publish date to current date.
		 */
		if ( isset( $data['post_date'] ) ) {
			$post_modified         = current_time( 'mysql' );
			$post_modified_gmt     = current_time( 'mysql', 1 );
			$data['post_date']     = $post_modified;
			$data['post_date_gmt'] = $post_modified_gmt;
		}

		return $data;
	}

	/**
	 * Different MIME type of different PHP version
	 *
	 * Filters the "real" file type of the given file.
	 *
	 * @since 1.2.9
	 *
	 * @param array  $defaults File data array containing 'ext', 'type', and
	 *                                          'proper_filename' keys.
	 * @param string $file                      Full path to the file.
	 * @param string $filename                  The name of the file (may differ from $file due to
	 *                                          $file being in a tmp directory).
	 * @param array  $mimes                     Key is the file extension with value as the mime type.
	 */
	public function real_mime_types( $defaults, $file, $filename, $mimes ) {
		return $this->real_mimes( $defaults, $filename, $file );
	}

	/**
	 * Real Mime Type
	 *
	 * @since 1.2.15
	 *
	 * @param array  $defaults File data array containing 'ext', 'type', and
	 *                                          'proper_filename' keys.
	 * @param string $filename                  The name of the file (may differ from $file due to
	 *                                          $file being in a tmp directory).
	 * @param string $file file content.
	 */
	public function real_mimes( $defaults, $filename, $file ) {

		// Validate file extension using WordPress core function to prevent double extension attacks.
		$filetype = wp_check_filetype(
			$filename,
			array(
				'xml'  => 'text/xml',
				'json' => 'application/json',
				'svg'  => 'image/svg+xml',
				'svgz' => 'image/svg+xml',
			)
		);

		// Get actual file extension.
		$file_extension = pathinfo( $filename, PATHINFO_EXTENSION );

		// Reject files with no valid extension or mismatched extensions.
		if ( false === $filetype['type'] || empty( $file_extension ) ) {
			return $defaults;
		}

		// Set EXT and real MIME type only for the file name `wxr.xml`.
		// Ensure the actual extension is 'xml' to prevent double extension attacks like 'test.wxr.php'.
		if ( 'xml' === $file_extension && strpos( $filename, 'wxr' ) !== false ) {
			$defaults['ext']  = 'xml';
			$defaults['type'] = 'text/xml';
		}

		// Set EXT and real MIME type only for the file name `wpforms.json`, `cartflows.json`, or `spectra.json`.
		// Ensure the actual extension is 'json' to prevent double extension attacks.
		if ( 'json' === $file_extension && ( strpos( $filename, 'wpforms' ) !== false || strpos( $filename, 'cartflows' ) !== false || strpos( $filename, 'spectra' ) !== false ) ) {
			$defaults['ext']  = 'json';
			$defaults['type'] = 'text/plain';
		}

		if ( 'svg' === $file_extension ) {
			// Perform SVG sanitization using the sanitize_svg function.
			$svg_content           = file_get_contents( $file ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
			$sanitized_svg_content = $this->sanitize_svg( $svg_content );
			// phpcs:disable WordPress.WP.AlternativeFunctions.file_system_read_file_put_contents
			file_put_contents( $file, $sanitized_svg_content );
			// phpcs:enable WordPress.WP.AlternativeFunctions.file_system_read_file_put_contents

			// Update mime type and extension.
			$defaults['type'] = 'image/svg+xml';
			$defaults['ext']  = 'svg';
		}

		if ( 'svgz' === $file_extension ) {
			// Perform SVG sanitization using the sanitize_svg function.
			$svg_content     = file_get_contents( $file ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
			$decoded_content = gzdecode( $svg_content );

			if ( false !== $decoded_content ) {
				$svg_content = $decoded_content;
			}
			$sanitized_svg_content = $this->sanitize_svg( $svg_content );

			if ( false !== $decoded_content ) {
				$sanitized_svg_content = gzencode( $sanitized_svg_content );
			}

			// phpcs:disable WordPress.WP.AlternativeFunctions.file_system_read_file_put_contents
			file_put_contents( $file, $sanitized_svg_content );
			// phpcs:enable WordPress.WP.AlternativeFunctions.file_system_read_file_put_contents

			// Update mime type and extension.
			$defaults['type'] = 'image/svg+xml';
			$defaults['ext']  = 'svgz';
		}

		return $defaults;
	}

	/**
	 * Different MIME type of different PHP version
	 *
	 * Filters the "real" file type of the given file.
	 *
	 * @since 1.2.9
	 *
	 * @param array  $defaults File data array containing 'ext', 'type', and
	 *                                          'proper_filename' keys.
	 * @param string $file                      Full path to the file.
	 * @param string $filename                  The name of the file (may differ from $file due to
	 *                                          $file being in a tmp directory).
	 * @param array  $mimes                     Key is the file extension with value as the mime type.
	 * @param string $real_mime                Real MIME type of the uploaded file.
	 */
	public function real_mime_types_5_1_0( $defaults, $file, $filename, $mimes, $real_mime ) {
		return $this->real_mimes( $defaults, $filename, $file );
	}

	/**
	 * Sanitizes SVG Code string.
	 *
	 * @param string $original_content SVG code to sanitize.
	 * @return string|bool
	 * @since 1.0.7
	 * @phpstan-ignore-next-line
	 * */
	public function sanitize_svg( $original_content ) {

		if ( ! $original_content ) {
			return '';
		}

		// Define allowed tags and attributes.
		$allowed_tags = array(
			'a',
			'circle',
			'clippath',
			'defs',
			'style',
			'desc',
			'ellipse',
			'fegaussianblur',
			'filter',
			'foreignobject',
			'g',
			'image',
			'line',
			'lineargradient',
			'marker',
			'mask',
			'metadata',
			'path',
			'pattern',
			'polygon',
			'polyline',
			'radialgradient',
			'rect',
			'stop',
			'svg',
			'switch',
			'symbol',
			'text',
			'textpath',
			'title',
			'tspan',
			'use',
		);

		$allowed_attributes = array(
			'class',
			'clip-path',
			'clip-rule',
			'fill',
			'fill-opacity',
			'fill-rule',
			'filter',
			'id',
			'mask',
			'opacity',
			'stroke',
			'stroke-dasharray',
			'stroke-dashoffset',
			'stroke-linecap',
			'stroke-linejoin',
			'stroke-miterlimit',
			'stroke-opacity',
			'stroke-width',
			'style',
			'systemlanguage',
			'transform',
			'href',
			'xlink:href',
			'xlink:title',
			'cx',
			'cy',
			'r',
			'requiredfeatures',
			'clippathunits',
			'type',
			'rx',
			'ry',
			'color-interpolation-filters',
			'stddeviation',
			'filterres',
			'filterunits',
			'height',
			'primitiveunits',
			'width',
			'x',
			'y',
			'font-size',
			'display',
			'font-family',
			'font-style',
			'font-weight',
			'text-anchor',
			'marker-end',
			'marker-mid',
			'marker-start',
			'x1',
			'x2',
			'y1',
			'y2',
			'gradienttransform',
			'gradientunits',
			'spreadmethod',
			'markerheight',
			'markerunits',
			'markerwidth',
			'orient',
			'preserveaspectratio',
			'refx',
			'refy',
			'viewbox',
			'maskcontentunits',
			'maskunits',
			'd',
			'patterncontentunits',
			'patterntransform',
			'patternunits',
			'points',
			'fx',
			'fy',
			'offset',
			'stop-color',
			'stop-opacity',
			'xmlns',
			'xmlns:se',
			'xmlns:xlink',
			'xml:space',
			'method',
			'spacing',
			'startoffset',
			'dx',
			'dy',
			'rotate',
			'textlength',
		);

		$is_encoded = false;

		$needle = "\x1f\x8b\x08";
		// phpcs:disable PHPCompatibility.ParameterValues.NewIconvMbstringCharsetDefault.NotSet
		if ( function_exists( 'mb_strpos' ) ) {
			$is_encoded = 0 === mb_strpos( $original_content, $needle );
		} else {
			$is_encoded = 0 === strpos( $original_content, $needle );
		}
		// phpcs:enable PHPCompatibility.ParameterValues.NewIconvMbstringCharsetDefault.NotSet

		// phpcs:disable WordPress.PHP.YodaConditions.NotYoda
		if ( $is_encoded ) {
			$original_content = gzdecode( $original_content );
			if ( $original_content === false ) {
				return '';
			}
		}
		// phpcs:enable WordPress.PHP.YodaConditions.NotYoda

		// Strip php tags.
		$content = preg_replace( '/<\?(=|php)(.+?)\?>/i', '', $original_content ); // phpcs:ignore Generic.PHP.ForbiddenFunctions.FoundWithAlternative -- -- 3rd party library.
		$content = preg_replace( '/<\?(.*)\?>/Us', '', $content ); // phpcs:ignore Generic.PHP.ForbiddenFunctions.FoundWithAlternative -- -- 3rd party library.
		$content = preg_replace( '/<\%(.*)\%>/Us', '', $content ); // phpcs:ignore Generic.PHP.ForbiddenFunctions.FoundWithAlternative -- -- 3rd party library.

		if ( ( false !== strpos( $content, '<?' ) ) || ( false !== strpos( $content, '<%' ) ) ) {
			return '';
		}

		// Strip comments.
		$content = preg_replace( '/<!--(.*)-->/Us', '', $content ); // phpcs:ignore Generic.PHP.ForbiddenFunctions.FoundWithAlternative -- -- 3rd party library.
		$content = preg_replace( '/\/\*(.*)\*\//Us', '', $content ); // phpcs:ignore Generic.PHP.ForbiddenFunctions.FoundWithAlternative -- -- 3rd party library.

		if ( ( false !== strpos( $content, '<!--' ) ) || ( false !== strpos( $content, '/*' ) ) ) {
			return '';
		}

		// Strip line breaks.
		$content = preg_replace( '/\r|\n/', '', $content ); // phpcs:ignore Generic.PHP.ForbiddenFunctions.FoundWithAlternative -- -- 3rd party library.

		// Find the start and end tags so we can cut out miscellaneous garbage.
		$start = strpos( $content, '<svg' );
		$end   = strrpos( $content, '</svg>' );
		if ( false === $start || false === $end ) {
			return '';
		}

		$content = substr( $content, $start, ( $end - $start + 6 ) );

		// If the server's PHP version is 8 or up, make sure to disable the ability to load external entities.
		$php_version_under_eight = version_compare( PHP_VERSION, '8.0.0', '<' );
		if ( $php_version_under_eight ) {
			// phpcs:disable Generic.PHP.DeprecatedFunctions.Deprecated
			$libxml_disable_entity_loader = libxml_disable_entity_loader( true );
			// phpcs:enable Generic.PHP.DeprecatedFunctions.Deprecated
		}
		// Suppress the errors.
		$libxml_use_internal_errors = libxml_use_internal_errors( true );

		// Create DOMDocument instance.
		$dom = new \DOMDocument();
		// phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
		$dom->formatOutput        = false;
		$dom->preserveWhiteSpace  = false;
		$dom->strictErrorChecking = false;
		// phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase

		$open_svg = ! ! $content ? $dom->loadXML( $content ) : false;
		if ( ! $open_svg ) {
			return '';
		}

		// Strip Doctype.
		// phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
		foreach ( $dom->childNodes as $child ) {
			if ( XML_DOCUMENT_TYPE_NODE === $child->nodeType && ! ! $child->parentNode ) {
				$child->parentNode->removeChild( $child );
		// phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
			}
		}

		// Sanitize elements.
		// phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase, WordPress.PHP.StrictInArray
		$elements = $dom->getElementsByTagName( '*' );
		for ( $index = $elements->length - 1; $index >= 0; $index-- ) {
			$current_element = $elements->item( $index );
			if ( ! in_array( strtolower( $current_element->tagName ), $allowed_tags ) ) {
				$current_element->parentNode->removeChild( $current_element );
				// phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
				continue;
			}

			// Validate allowed attributes.
			for ( $i = $current_element->attributes->length - 1; $i >= 0; $i-- ) {
				$attr_name           = $current_element->attributes->item( $i )->name;
				$attr_name_lowercase = strtolower( $attr_name );
				if ( ! in_array( $attr_name_lowercase, $allowed_attributes ) &&
					! preg_match( '/^aria-/', $attr_name_lowercase ) &&
					! preg_match( '/^data-/', $attr_name_lowercase ) ) {
					$current_element->removeAttribute( $attr_name );
					continue;
				}

				$attr_value = $current_element->attributes->item( $i )->value;
				if ( ! empty( $attr_value ) &&
					( preg_match( '/^((https?|ftp|file):)?\/\//i', $attr_value ) ||
					preg_match( '/base64|data|(?:java)?script|alert\(|window\.|document/i', $attr_value ) ) ) {
					$current_element->removeAttribute( $attr_name );
					continue;
				}
			}

			// Strip xlink:href.
			$xlink_href = $current_element->getAttributeNS( 'http://www.w3.org/1999/xlink', 'href' );
			if ( $xlink_href && strpos( $xlink_href, '#' ) !== 0 ) {
				$current_element->removeAttributeNS( 'http://www.w3.org/1999/xlink', 'href' );
			}

			// Strip use tag with external references.
			// phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
			if ( strtolower( $current_element->tagName ) === 'use' ) {
				$xlink_href = $current_element->getAttributeNS( 'http://www.w3.org/1999/xlink', 'href' );
				if ( $current_element->parentNode && $xlink_href && strpos( $xlink_href, '#' ) !== 0 ) {
					$current_element->parentNode->removeChild( $current_element );
					// phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
				}
			}
		}

		// phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
		$sanitized = $dom->saveXML( $dom->documentElement, LIBXML_NOEMPTYTAG );
		// phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase

		// Restore defaults.
		if ( $php_version_under_eight && isset( $libxml_disable_entity_loader ) ) {
			// phpcs:disable Generic.PHP.DeprecatedFunctions.Deprecated
			libxml_disable_entity_loader( $libxml_disable_entity_loader );
			// phpcs:enable Generic.PHP.DeprecatedFunctions.Deprecated, WordPress.PHP.StrictInArray
		}
		libxml_use_internal_errors( $libxml_use_internal_errors );

		return $sanitized;
	}

	/**
	 * Download File Into Uploads Directory
	 *
	 * @since 1.0.0 Added $overrides argument to override the uploaded file actions.
	 *
	 * @param  string $file Download File URL.
	 * @param  array  $overrides Upload file arguments.
	 * @param  int    $timeout_seconds Timeout in downloading the XML file in seconds.
	 * @return array        Downloaded file data.
	 */
	public static function download_file( $file = '', $overrides = array(), $timeout_seconds = 300 ) {
		// Log file download attempt.
		ST_Importer_Log::add(
			'info',
			'Attempting to download file: ' . $file,
			array(
				'file_url'        => $file,
				'timeout_seconds' => $timeout_seconds,
			)
		);

		// Gives us access to the download_url() and wp_handle_sideload() functions.
		require_once ABSPATH . 'wp-admin/includes/file.php';

		// Download file to temp dir.
		$temp_file = download_url( $file, $timeout_seconds );

		// WP Error.
		if ( is_wp_error( $temp_file ) ) {
			// Log download failure.
			ST_Importer_Log::add(
				'error',
				'File download failed: ' . $temp_file->get_error_message(),
				array(
					'file_url'      => $file,
					'error_message' => $temp_file->get_error_message(),
					'error_code'    => $temp_file->get_error_code(),
				)
			);

			return array(
				'success' => false,
				'data'    => $temp_file->get_error_message(),
			);
		}

		// Array based on $_FILE as seen in PHP file uploads.
		$file_args = array(
			'name'     => basename( $file ),
			'tmp_name' => $temp_file,
			'error'    => 0,
			'size'     => filesize( $temp_file ),
		);

		$defaults = array(

			// Tells WordPress to not look for the POST form
			// fields that would normally be present as
			// we downloaded the file from a remote server, so there
			// will be no form fields
			// Default is true.
			'test_form'   => false,

			// Setting this to false lets WordPress allow empty files, not recommended.
			// Default is true.
			'test_size'   => true,

			// A properly uploaded file will pass this test. There should be no reason to override this one.
			'test_upload' => true,

			'mimes'       => array(
				'xml'  => 'text/xml',
				'json' => 'application/json',
			),
		);

		$overrides = wp_parse_args( $overrides, $defaults );

		// Move the temporary file into the uploads directory.
		$results = wp_handle_sideload( $file_args, $overrides );

		if ( isset( $results['error'] ) ) {
			// Log sideload failure.
			ST_Importer_Log::add(
				'error',
				'File sideload failed: ' . ( isset( $results['error'] ) ? $results['error'] : 'Unknown error' ),
				array(
					'file_url'  => $file,
					'file_size' => $file_args['size'],
					'error'     => isset( $results['error'] ) ? $results['error'] : '',
				)
			);

			return array(
				'success' => false,
				'data'    => $results,
			);
		}

		// Log successful download.
		ST_Importer_Log::add(
			'success',
			'File downloaded and sideloaded successfully: ' . $file,
			array(
				'file_url'      => $file,
				'file_size'     => $file_args['size'],
				'uploaded_file' => isset( $results['file'] ) ? $results['file'] : '',
			)
		);

		// Success.
		return array(
			'success' => true,
			'data'    => $results,
		);
	}

	/**
	 * Start the xml import.
	 *
	 * @since  1.0.0
	 *
	 * @param  string $path Absolute path to the XML file.
	 * @param  int    $post_id Uploaded XML file ID.
	 */
	public static function get_xml_data( $path, $post_id ) {
		// Log XML parsing start.
		ST_Importer_Log::add(
			'info',
			'Starting to parse XML file for import metadata',
			array(
				'xml_file_path' => $path,
				'xml_post_id'   => $post_id,
			)
		);

		$args = array(
			'action'      => 'astra-wxr-import',
			'id'          => '1',
			'_ajax_nonce' => wp_create_nonce( 'astra-sites' ),
			'xml_id'      => $post_id,
		);
		$url  = add_query_arg( urlencode_deep( $args ), admin_url( 'admin-ajax.php', 'relative' ) );

		$data = self::get_data( $path );

		// Check if XML parsing resulted in an error.
		if ( is_wp_error( $data ) ) {
			ST_Importer_Log::add(
				'error',
				'XML parsing failed: ' . $data->get_error_message(),
				array(
					'xml_file_path' => $path,
					'error_message' => $data->get_error_message(),
					'error_code'    => $data->get_error_code(),
				)
			);

			return $data;
		}

		// Log XML metadata parsed successfully.
		ST_Importer_Log::add(
			'success',
			'XML file parsed successfully with import counts',
			array(
				'xml_file_path' => $path,
				'post_count'    => $data->post_count,
				'media_count'   => $data->media_count,
				'author_count'  => count( $data->users ),
				'comment_count' => $data->comment_count,
				'term_count'    => $data->term_count,
			)
		);

		return array(
			'count'   => array(
				'posts'    => $data->post_count,
				'media'    => $data->media_count,
				'users'    => count( $data->users ),
				'comments' => $data->comment_count,
				'terms'    => $data->term_count,
			),
			'url'     => $url,
			'strings' => array(
				'complete' => __( 'Import complete!', 'astra-sites' ),
			),
		);
	}

	/**
	 * Get XML data.
	 *
	 * @since 1.0.0
	 * @param  string $url Downloaded XML file absolute URL.
	 * @return object  XML file data.
	 */
	public static function get_data( $url ) {
		$importer = self::get_importer();
		$data     = $importer->get_preliminary_information( $url );
		if ( is_wp_error( $data ) ) {
			return $data;
		}
		return $data;
	}

	/**
	 * Get Importer
	 *
	 * @since 1.0.0
	 * @return \WXR_Importer WXR_Importer object.
	 */
	public static function get_importer() {
		$options = apply_filters(
			'astra_sites_xml_import_options',
			array(
				'update_attachment_guids' => true,
				'fetch_attachments'       => true,
				'default_author'          => get_current_user_id(),
			)
		);

		$importer = new \WXR_Importer( $options );
		$logger   = new \WP_Importer_Logger_ServerSentEvents();

		$importer->set_logger( $logger );
		return $importer;
	}

	/**
	 * Check is valid URL
	 *
	 * @param string $url  The site URL.
	 *
	 * @since 2.7.1
	 * @return string
	 */
	public static function is_valid_wxr_url( $url = '' ) {
		if ( empty( $url ) ) {
			// Log URL validation error - empty URL.
			ST_Importer_Log::add(
				'warning',
				'URL validation failed: Empty or missing URL provided',
				array(
					'url' => $url,
				)
			);

			return false;
		}

		$parse_url = wp_parse_url( $url );
		if ( empty( $parse_url ) || ! is_array( $parse_url ) ) {
			// Log URL validation error - invalid URL format.
			ST_Importer_Log::add(
				'warning',
				'URL validation failed: Invalid URL format or unable to parse',
				array(
					'url'       => $url,
					'parse_url' => $parse_url,
				)
			);

			return false;
		}

		$valid_hosts = apply_filters(
			'astra_sites_valid_url',
			array(
				'lh3.googleusercontent.com',
				'pixabay.com',
			)
		);

		$ai_site_url = get_option( 'ast_ai_import_current_url', '' );

		if ( '' !== $ai_site_url ) {
			$url           = wp_parse_url( $ai_site_url );
			$valid_hosts[] = $url ? $url['host'] : '';
		}

		$api_domain_parse_url = wp_parse_url( ST_Importer_Helper::get_api_domain() );
		$valid_hosts[]        = $api_domain_parse_url['host'];

		// Validate host.
		if ( in_array( $parse_url['host'], $valid_hosts, true ) ) {
			// Log successful URL validation.
			ST_Importer_Log::add(
				'info',
				'URL validation passed: Host is allowed',
				array(
					'url'  => $url,
					'host' => $parse_url['host'],
				)
			);

			return true;
		}

		// Log URL validation failure - host not in whitelist.
		ST_Importer_Log::add(
			'warning',
			'URL validation failed: Host not in whitelist',
			array(
				'url'         => $url,
				'host'        => $parse_url['host'],
				'valid_hosts' => implode( ', ', $valid_hosts ),
			)
		);

		return false;
	}

		/**
		 * Send message when a post has been imported.
		 *
		 * @since 1.1.0
		 * @param int   $id Post ID.
		 * @param array $data Post data saved to the DB.
		 */
	public function imported_post( $id, $data ) {
		$this->emit_sse_message(
			array(
				'action' => 'updateDelta',
				'type'   => ( 'attachment' === $data['post_type'] ) ? 'media' : 'posts',
				'delta'  => 1,
			)
		);
	}

	/**
	 * Send message when a post is marked as already imported.
	 *
	 * @since 1.1.0
	 * @param array $data Post data saved to the DB.
	 */
	public function already_imported_post( $data ) {
		$this->emit_sse_message(
			array(
				'action' => 'updateDelta',
				'type'   => ( 'attachment' === $data['post_type'] ) ? 'media' : 'posts',
				'delta'  => 1,
			)
		);
	}

	/**
	 * Send message when a comment has been imported.
	 *
	 * @since 1.1.0
	 */
	public function imported_comment() {
		$this->emit_sse_message(
			array(
				'action' => 'updateDelta',
				'type'   => 'comments',
				'delta'  => 1,
			)
		);
	}

	/**
	 * Send message when a term has been imported.
	 *
	 * @since 1.1.0
	 */
	public function imported_term() {
		$this->emit_sse_message(
			array(
				'action' => 'updateDelta',
				'type'   => 'terms',
				'delta'  => 1,
			)
		);
	}

	/**
	 * Send message when a user has been imported.
	 *
	 * @since 1.1.0
	 */
	public function imported_user() {
		$this->emit_sse_message(
			array(
				'action' => 'updateDelta',
				'type'   => 'users',
				'delta'  => 1,
			)
		);
	}

	/**
	 * Emit a Server-Sent Events message.
	 *
	 * @since 1.1.0
	 * @param mixed $data Data to be JSON-encoded and sent in the message.
	 */
	public function emit_sse_message( $data ) {

		if ( wp_doing_ajax() ) {
			echo "event: message\n";
			echo 'data: ' . wp_json_encode( $data ) . "\n\n";

			// Extra padding.
			echo esc_html( ':' . str_repeat( ' ', 2048 ) . "\n\n" );
		}

		flush();
	}
	/**
	 * Track Imported Post
	 *
	 * @param  int   $post_id Post ID.
	 * @param array $data Raw data imported for the post.
	 * @return void
	 */
	public function track_post( $post_id = 0, $data = array() ) {
		ST_Importer_Helper::track_post( $post_id, $data );
	}
}
