STRATO-apps/wordpress_03/app/wp-content/plugins/tutor-pro/tools/handlers/AjaxHandler.php

SHA-256: 0498e9e6d48967e15cadace10eb45cd8cf2a23c31fdef60c5470d1901be70668
<?php
/**
 * TutorPro Exporter
 *
 * @package TutorPro\Tools
 * @author  Themeum<support@themeum.com>
 * @link    https://themeum.com
 * @since   3.6.0
 */

namespace TutorPro\Tools;

use TUTOR\User;
use TUTOR\Input;
use TUTOR\Course;
use Tutor\Options_V2;
use AllowDynamicProperties;
use Tutor\Models\UserModel;
use Tutor\Helpers\HttpHelper;
use Tutor\Models\CourseModel;
use Tutor\Helpers\QueryHelper;
use Tutor\Traits\JsonResponse;
use Tutor\Helpers\ValidationHelper;
use TutorPro\CourseBundle\Models\BundleModel;
use TutorPro\ContentBank\Models\CollectionModel;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Handling export functionality.
 *
 * @since 3.6.0
 */
#[AllowDynamicProperties]
class AjaxHandler {

	use JsonResponse;

	/**
	 * Directory to keep the export file
	 *
	 * @since 3.6.0
	 *
	 * @var string file path
	 */
	private $upload_dir;

	/**
	 * Exporter instance
	 *
	 * @since 3.6.0
	 *
	 * @var Exporter
	 */
	private $exporter;

	/**
	 * User model instance
	 *
	 * @since 3.8.1
	 *
	 * @var UserModel
	 */
	private UserModel $user_model;

	/**
	 * The type of content being processed (e.g., 'course', 'bundle').
	 *
	 * @since 3.8.1
	 *
	 * @var string
	 */
	private $content_type;

	/**
	 * The actual content data associated with the current content type.
	 *
	 * @since 3.8.1
	 *
	 * @var array
	 */
	private $content_data;


	/**
	 * The ID of the parent item currently being processed.
	 *
	 * @since 3.8.1
	 *
	 * @var int
	 */
	private $current_processing_parent_id;

	/**
	 * Unique identifier for the current job or export/import process.
	 *
	 * @since 3.8.1
	 *
	 * @var int
	 */
	private $job_id;

	/**
	 * The ID of the current course within a bundle being processed.
	 *
	 * @since 3.8.1
	 *
	 * @var int
	 */
	private $current_bundle_course_id;

	const SETTINGS = 'settings';

	/**
	 * Instance of the course model for interacting with course data.
	 *
	 * @since 3.6.0
	 *
	 * @var CourseModel
	 */
	private $course_model;

	/**
	 * Register hooks
	 *
	 * @since 3.6.0
	 * @since 3.9.3 param $register_hook added.
	 *
	 * @param Exporter    $exporter Export object.
	 * @param Importer    $importer Import object.
	 * @param CourseModel $course_model Course model object.
	 * @param bool        $register_hook whether to enable hooks or not.
	 */
	public function __construct( Exporter $exporter, Importer $importer, CourseModel $course_model, $register_hook = true ) {

		$this->exporter              = $exporter;
		$this->course_model          = $course_model;
		$this->importer              = $importer;
		$this->user_model            = new UserModel();
		$this->course_exporter       = tutor_pro_tools()->course_exporter;
		$this->bundle_exporter       = tutor_pro_tools()->bundle_exporter;
		$this->subscription_exporter = tutor_pro_tools()->subscription_exporter;

		if ( ! $register_hook ) {
			return;
		}

		add_action( 'wp_ajax_tutor_pro_exportable_contents', array( $this, 'ajax_get_exportable_contents' ) );
		add_action( 'wp_ajax_tutor_pro_export', array( $this, 'ajax_export_handler' ) );
		add_action( 'wp_ajax_tutor_pro_export_import_history', array( $this, 'ajax_fetch_history' ) );
		add_action( 'wp_ajax_tutor_pro_import', array( $this, 'ajax_import_handler' ) );
		add_action( 'wp_ajax_tutor_pro_delete_export_import_history', array( $this, 'ajax_delete_export_import_history' ) );
		add_action( 'tutor_pro_export_completed', array( $this, 'update_settings_log' ), 10, 2 );
		add_action( 'admin_post_tutor_pro_export_download', array( $this, 'handle_export_zip_download' ) );

		// Check if the 'init' action has already been fired.
		did_action( 'init' ) ? $this->set_upload_dir() : add_action( 'init', array( $this, 'set_upload_dir' ) );
	}

	/**
	 * Set the upload directory for the tutor-pro plugin
	 *
	 * @since 3.8.1
	 *
	 * @return void
	 */
	public function set_upload_dir() {
		$this->upload_dir = wp_upload_dir()['basedir'] . '/tutor-pro/';
	}

	/**
	 * Ajax handler for exportable contents API
	 *
	 * @since 3.6.0
	 *
	 * @return void send wp_json response
	 */
	public function ajax_get_exportable_contents() {
		tutor_utils()->check_nonce();
		tutor_utils()->check_current_user_capability( 'edit_tutor_course' );
		$default_course_ids = array();

		if ( ! User::is_admin() && tutor_utils()->is_instructor( 0, true ) ) {
			$default_course_ids = array_column( CourseModel::get_courses_by_instructor( 0, Course::course_status_list(), 0, PHP_INT_MAX, false ), 'ID' );
		}

		$course_ids = Input::post( 'course_ids', $default_course_ids, Input::TYPE_ARRAY );
		$context    = Input::post( 'context', '' );

		$contents = $this->get_exportable_content_with_count( $course_ids, $context );

		$this->json_response( __( 'Exportable contents fetched successfully!', 'tutor-pro' ), $contents, HttpHelper::STATUS_OK );
	}

	/**
	 * Get exportable contents with count
	 *
	 * @since 3.6.0
	 *
	 * @since 3.7.1 Course ids param added
	 *
	 * @param array  $course_ids Array of course ids.
	 * @param string $context Export context, content-bank or empty.
	 *
	 * @return array
	 */
	public function get_exportable_content_with_count( array $course_ids = array(), string $context = '' ) {
		$args = array(
			'post_type'              => tutor()->course_post_type,
			'posts_per_page'         => -1,
			'no_found_rows'          => true,     // Skip pagination counting.
			'update_post_term_cache' => false,   // Skip taxonomy term caching.
			'update_post_meta_cache' => false,   // Skip post meta caching.
			'fields'                 => 'ids',
			'post_status'            => 'any',
		);

		if ( count( $course_ids ) ) {
			$args['post__in'] = $course_ids;
		}

		$contents = $this->exporter->get_exportable_content( $context );

		foreach ( $contents as $key => $content ) {
			switch ( $content['key'] ) {
				case tutor()->course_post_type:
					$query = $this->course_model::get_courses_by_args( $args );

					$post_count = is_a( $query, 'WP_Query' ) ? $query->post_count : 0;
					$ids        = is_a( $query, 'WP_Query' ) ? $query->posts : array();

					$label = $content['label'];

					$contents[ $key ]['label'] = $label;
					$contents[ $key ]['ids']   = $ids;
					$contents[ $key ]['count'] = $post_count;

					$sub_contents = $this->get_contextual_sub_content( $context, isset( $content['contents'] ) ? $content['contents'] : array() );

					foreach ( $sub_contents as $k => $sub_content ) {
						$sub_content_count    = $this->course_model::count_course_content( $sub_content['key'], $course_ids );
						$sub_content['count'] = $sub_content_count;

						$sub_contents[ $k ] = $sub_content;
					}

					$contents[ $key ]['contents'] = $sub_contents;
					break;
				case tutor()->bundle_post_type:
					unset( $args['post__in'] );
					$args['post_type'] = tutor()->bundle_post_type;

					$query = $this->course_model::get_courses_by_args( $args );

					$post_count = is_a( $query, 'WP_Query' ) ? $query->post_count : 0;
					$ids        = is_a( $query, 'WP_Query' ) ? $query->posts : array();

					$label = $content['label'];

					$contents[ $key ]['label'] = $label;
					$contents[ $key ]['ids']   = $ids;
					$contents[ $key ]['count'] = $post_count;
					break;
				case $this->exporter::TYPE_CONTENT_BANK:
					unset( $args['post__in'] );
					$args['post_type'] = CollectionModel::POST_TYPE;

					$query = $this->course_model::get_courses_by_args( $args );

					$post_count = is_a( $query, 'WP_Query' ) ? $query->post_count : 0;
					$ids        = is_a( $query, 'WP_Query' ) ? $query->posts : array();

					$label = $content['label'];

					$contents[ $key ]['label'] = $label;
					$contents[ $key ]['ids']   = $ids;
					$contents[ $key ]['count'] = $post_count;
					break;

				default:
					// code...
					break;
			}
		}

		return $contents;
	}

	/**
	 * Handle export
	 *
	 * @since 3.6.0
	 *
	 * @since 3.8.1 User Data Added
	 *
	 * @return void wp_json response
	 */
	public function ajax_export_handler() {
		tutor_utils()->check_nonce();
		tutor_utils()->check_current_user_capability();

		$job_id           = Input::post( 'job_id', 0, Input::TYPE_INT );
		$export_contents  = $_POST['export_contents'] ?? array(); //phpcs:ignore
		$keep_media_files = Input::post( 'keep_media_files', '', Input::TYPE_INT );
		$keep_user_data   = Input::post( 'keep_user_data', '', Input::TYPE_INT );

		$contents = array();
		foreach ( $export_contents as $value ) {
			$contents[] = json_decode( stripslashes( $value ), true );
		}

		// Add Membership Plan contents if user data option is selected.
		if ( $keep_user_data && ! $job_id ) {
			$contents = $this->exporter->append_export_contents( $contents );
		}

		if ( $job_id ) {
			$existing_job = $this->get_export_job( $job_id );
			$contents     = $existing_job['job_requirements'] ?? array();
		}

		if ( ! $job_id && ! $contents ) {
			$this->response_bad_request( __( 'Invalid request!', 'tutor-pro' ) );
		}

		$contents = array_map(
			function ( $content ) {
				$type = Input::sanitize( $content['type'] );
				$ids  = array_map( 'intval', $content['ids'] ?? array() );

				$content['ids']  = $ids;
				$content['type'] = $type;

				return $content;
			},
			$contents
		);

		// Get or create job data.
		$job_data = $this->get_export_job( $job_id ) ?? $this->get_default_job_data( $job_id, $contents );

		$this->job_id = $job_data['job_id'];

		$exporter = $this->exporter;
		$this->exporter->add_job_id( $this->job_id );

		// Process contents in batches.
		foreach ( $contents as $content ) {
			$type = $content['type'] ?? false;
			$ids  = $content['ids'] ?? array();

			// Skip already processed items.
			if ( $exporter::TYPE_SETTINGS === $type && ( $job_data['completed_contents']['settings'] ?? false ) ) {
				continue;
			}

			// Process one item at a time.
			if ( tutor()->course_post_type === $type && ! empty( $ids ) ) {

				$this->prepare_job_data( $job_data, $keep_media_files, $keep_user_data );

				$completed_contents                                = $job_data['completed_contents'][ tutor()->course_post_type ];
				$course_sub_files                                  = $this->course_exporter->get_sub_files( $job_data['keep_user_data'] );
				[$remaining_course_sub_files, $current_sub_file  ] = $this->get_current_and_remaining_sub_files( $completed_contents, $course_sub_files );
				$remaining_ids                                     = $this->get_remaining_ids( $ids, $completed_contents );

				if ( ! empty( $remaining_ids ) ) {
					$is_failed     = false;
					$id_to_process = $exporter::COURSE === $current_sub_file ? array_shift( $remaining_ids ) : $completed_contents['current_processing_id'];
					$sub_contents  = $content['sub_contents'] ?? array();
					$export_data   = null;

					try {
						$export_data = $exporter->add_courses( $id_to_process, $sub_contents, $current_sub_file )->export();

					} catch ( \Throwable $th ) {
						$is_failed = true;
					} finally {
						if ( $is_failed ) {
							$job_data['completed_contents'][ tutor()->course_post_type ]['failed'][] = $id_to_process;
						}
						// if all sub files of a course are exported.
						if ( empty( $remaining_course_sub_files ) ) {
							$job_data['completed_contents'][ tutor()->course_post_type ]['success'][] = $id_to_process;
						}
						$job_data['completed_contents'][ tutor()->course_post_type ]['current_processing_id'] = $id_to_process;
						$job_data['completed_contents'][ tutor()->course_post_type ]['remaining_sub_files']   = $remaining_course_sub_files;
					}

					if ( is_null( $export_data ) ) {
						$export_data = $this->exporter->get_schema();
					}

					try {
						$this->update_export_job( $job_data, tutor()->course_post_type, $export_data );
					} catch ( \Throwable $th ) {
						$this->send_export_response( __( 'Course export failed', 'tutor-pro' ), HttpHelper::STATUS_BAD_REQUEST );
					}

					$this->send_export_response( __( 'Export in progress', 'tutor-pro' ) );
				}
			} elseif ( tutor()->bundle_post_type === $type && ! empty( $ids ) ) {

				$this->prepare_job_data( $job_data, $keep_media_files, $keep_user_data );

				$completed_contents                                = $job_data['completed_contents'][ tutor()->bundle_post_type ];
				$bundle_sub_files                                  = $this->bundle_exporter->get_sub_files( $job_data['keep_user_data'] );
				[$remaining_bundle_sub_files, $current_sub_file  ] = $this->get_current_and_remaining_sub_files( $completed_contents, $bundle_sub_files );
				$remaining_ids                                     = $this->get_remaining_ids( $ids, $completed_contents );

				if ( ! empty( $remaining_ids ) ) {

					$is_failed     = false;
					$id_to_process = $exporter::BUNDLE === $current_sub_file ? array_shift( $remaining_ids ) : $completed_contents['current_processing_id'];
					$export_data   = null;

					try {
						if ( $exporter::COURSES === $current_sub_file ) {

							$next_course_state           = $this->prepare_next_course_in_bundle( $job_data, $id_to_process, $completed_contents, $remaining_bundle_sub_files );
							$current_course_sub_file     = $next_course_state['current_course_sub_file'];
							$remaining_course_sub_files  = $next_course_state['remaining_course_sub_files'];
							$remaining_bundle_course_ids = $next_course_state['remaining_bundle_course_ids'];

							// If no course is added on bundle then skip the course part.
							if ( empty( $remaining_bundle_course_ids ) && $job_data['keep_user_data'] ) {
								$remaining_course_sub_files = array();
								$remaining_bundle_sub_files = array();
							}

							$exporter->set_current_bundle_course_id( $this->current_bundle_course_id );
							$exporter->set_current_course_sub_file( $current_course_sub_file );
						}

						$export_data = $exporter->add_bundles( $id_to_process, $current_sub_file )->export();

					} catch ( \Throwable $th ) {
						$is_failed = true;
					} finally {
						if ( $is_failed ) {
							$job_data['completed_contents'][ tutor()->bundle_post_type ]['failed'][] = $id_to_process;
						}

						if ( $exporter::COURSES === $current_sub_file ) {

							// Shift current course sub file as it is completed.
							array_shift( $remaining_course_sub_files );

							if ( empty( $remaining_course_sub_files ) ) {
								// Shift current bundle course id as it is completed.
								array_shift( $remaining_bundle_course_ids );
								$job_data['completed_contents'][ tutor()->bundle_post_type ][ tutor()->course_post_type ]['success'][] = $this->current_bundle_course_id;
							}

							$remaining_bundle_sub_files = array( $exporter::COURSES );
							if ( empty( $remaining_bundle_course_ids ) && empty( $remaining_course_sub_files ) ) {
								array_shift( $remaining_bundle_sub_files );
							}

							if ( empty( $remaining_bundle_sub_files ) ) {
								$job_data['completed_contents'][ tutor()->bundle_post_type ]['success'][] = $id_to_process;
							}

							$job_data['completed_contents'][ tutor()->bundle_post_type ][ tutor()->course_post_type ]['current_bundle_course_id']    = $this->current_bundle_course_id;
							$job_data['completed_contents'][ tutor()->bundle_post_type ][ tutor()->course_post_type ]['remaining_bundle_course_ids'] = $remaining_bundle_course_ids;
							$job_data['completed_contents'][ tutor()->bundle_post_type ][ tutor()->course_post_type ]['remaining_course_sub_files']  = $remaining_course_sub_files ?? array();
						}

						$job_data['completed_contents'][ tutor()->bundle_post_type ]['current_processing_id'] = $id_to_process;
						$job_data['completed_contents'][ tutor()->bundle_post_type ]['remaining_sub_files']   = $remaining_bundle_sub_files;

					}

					if ( is_null( $export_data ) ) {
						$export_data = $this->exporter->get_schema();
					}

					try {
						$this->update_export_job( $job_data, tutor()->bundle_post_type, $export_data );
					} catch ( \Throwable $th ) {
						$this->send_export_response( __( 'Bundle export failed', 'tutor-pro' ), HttpHelper::STATUS_BAD_REQUEST );
					}

					$this->send_export_response( __( 'Export in progress', 'tutor-pro' ) );
				}
			} elseif ( $exporter::TYPE_CONTENT_BANK === $type && ! empty( $ids ) ) {
				$res = CollectionExporter::ajax_req_handler( $job_data, $ids, $exporter );

				$keep_media_files = $job_data['completed_contents'][ tutor()->course_post_type ]['keep_media_files'] ?? $keep_media_files ?? 0;

				if ( $keep_media_files ) {
					$exporter->add_media_files();
				}

				if ( ! $res->is_done ) {
					$job_data    = $res->job_data;
					$export_data = $res->export_data;
					try {
						$this->update_export_job( $job_data, $exporter::TYPE_CONTENT_BANK, $export_data );
					} catch ( \Throwable $th ) {
						$this->send_export_response( __( 'Content bank data export failed', 'tutor-pro' ), HttpHelper::STATUS_BAD_REQUEST );
					}

					$this->send_export_response( __( 'Export in progress', 'tutor-pro' ) );
				}
			} elseif ( $type === $exporter::TYPE_SETTINGS ) {
				$export_data = $exporter->add_settings()->export();

				$job_data['completed_contents']['settings'] = true;
				try {
					$this->update_export_job( $job_data, $exporter::TYPE_SETTINGS, $export_data );
				} catch ( \Throwable $th ) {
					$this->send_export_response( __( 'Settings export failed', 'tutor-pro' ), HttpHelper::STATUS_BAD_REQUEST );
				}

				$this->send_export_response( __( 'Export in progress', 'tutor-pro' ) );

			} elseif ( $type === $this->exporter::TYPE_MEMBERSHIP_PLANS && ! empty( $ids ) ) {

				$completed_contents                                    = $job_data['completed_contents'][ $this->exporter::TYPE_MEMBERSHIP_PLANS ];
				$membership_sub_files                                  = $this->subscription_exporter->get_sub_files();
				[$remaining_membership_sub_files, $current_sub_file  ] = $this->get_current_and_remaining_sub_files( $completed_contents, $membership_sub_files );
				$remaining_ids = $this->get_remaining_ids( $ids, $completed_contents );

				if ( ! empty( $remaining_ids ) ) {
					$is_failed     = false;
					$id_to_process = $exporter::PLANS === $current_sub_file ? array_shift( $remaining_ids ) : $completed_contents['current_processing_id'];

					$export_data = null;
					try {
						$export_data = $exporter->add_membership_plans( $id_to_process, $current_sub_file )->export();
					} catch ( \Throwable $th ) {
						$is_failed = true;
					} finally {
						if ( $is_failed ) {
							$job_data['completed_contents'][ $this->exporter::TYPE_MEMBERSHIP_PLANS ]['failed'][]              = $id_to_process;
							$job_data['completed_contents'][ $this->exporter::TYPE_MEMBERSHIP_PLANS ]['failed']['sub_files'][] = $current_sub_file;
						} else {

							// if all sub files of a membership is exported.
							if ( empty( $remaining_membership_sub_files ) ) {
								$job_data['completed_contents'][ $this->exporter::TYPE_MEMBERSHIP_PLANS ]['success'][] = $id_to_process;
							}
							$job_data['completed_contents'][ $this->exporter::TYPE_MEMBERSHIP_PLANS ]['current_processing_id'] = $id_to_process;
							$job_data['completed_contents'][ $this->exporter::TYPE_MEMBERSHIP_PLANS ]['remaining_sub_files']   = $remaining_membership_sub_files;
						}
					}

					if ( is_null( $export_data ) ) {
						$export_data = $this->exporter->get_schema();
					}

					try {
						$this->update_export_job( $job_data, $this->exporter::TYPE_MEMBERSHIP_PLANS, $export_data );
					} catch ( \Throwable $th ) {
						$this->send_export_response( __( 'Bundle export failed', 'tutor-pro' ), HttpHelper::STATUS_BAD_REQUEST );
					}

					$this->send_export_response( __( 'Export in progress', 'tutor-pro' ) );
				}
			} elseif ( $type === $exporter::GRADE_BOOKS_SETTINGS ) {

				$export_data = $exporter->add_grade_book_settings()->export();

				$job_data['completed_contents'][ $exporter::GRADE_BOOKS_SETTINGS ] = true;
				try {
					$this->update_export_job( $job_data, $exporter::GRADE_BOOKS_SETTINGS, $export_data );
				} catch ( \Throwable $th ) {
					$this->send_export_response( __( 'Settings export failed', 'tutor-pro' ), HttpHelper::STATUS_BAD_REQUEST );
				}

				$this->send_export_response( __( 'Export in progress', 'tutor-pro' ) );
			}
		}

		// If we get here, all items are processed.
		$progress = (int) $this->calculate_progress( $job_data );
		if ( 100 === $progress ) {
			$job_data['job_status'] = 'completed';
		}

		$this->send_export_response( 100 === $progress ? __( 'Export completed', 'tutor-pro' ) : __( 'Export in progress', 'tutor-pro' ) );
	}

	/**
	 * Obtain import data from json file.
	 *
	 * @since 3.8.1
	 *
	 * @throws \Exception Error reading json file.
	 * @throws \Throwable Exception when preparing import contents.
	 *
	 * @param array $file the json file contents.
	 * @param array $job_data the initial job data.
	 * @param int   $collection_id the collection id.
	 *
	 * @return array
	 */
	private function get_import_data_from_json( $file, $job_data, $collection_id ) {
		$contents = $this->read_import_file( $file, $job_data );

		if ( is_wp_error( $contents ) ) {
			throw new \Exception( $contents->get_error_message() );
		}

		$contents = json_decode( $contents, true );

		$this->job_id = $job_data['job_id'] ?? 0;

		if ( $contents ) {
			try {
				$job_data = $this->prepare_import_contents( $contents, $collection_id );
				unset( $contents );
			} catch ( \Throwable $th ) {
				throw $th;
			}
		}

		Helper::increase_memory_limit();

		$data = $this->get_json_data( 'import' );

		return array(
			'data'     => $data,
			'job_data' => $job_data,
		);
	}

	/**
	 * Get job requirements from the zip upload file.
	 *
	 * @throws \Exception If the json files are not valid.
	 *
	 * @since 3.8.1
	 *
	 * @param array  $directories the array of file directory path.
	 * @param string $path the main path.
	 * @param array  $json_validation_rules rules to validate json.
	 * @param string $import_file_type the import file type.
	 * @param array  $job_requirements the job requirement to update.
	 * @param array  $file_paths the import file json paths.
	 * @param int    $collection_id the collection id.
	 *
	 * @return array
	 */
	private function get_zip_import_job( $directories, $path, $json_validation_rules, $import_file_type = 'courses', $job_requirements = array(), $file_paths = array(), $collection_id = 0 ) {
		$updated_requirements = $job_requirements;
		$paths                = $file_paths;
		foreach ( $directories as $directory ) {
			$file_name = str_replace( $path . '/', '', $directory );
			$json_path = $directory . '/' . $file_name . '.json';
			$files     = glob( "$directory/*" );

			if ( ! in_array( $json_path, $files ) ) {
				throw new \Exception( __( 'The file provided is invalid', 'tutor-pro' ) );
			}

			$paths[ $json_path ] = $import_file_type;

			foreach ( $files as $file_path ) {
				$file_contents = file_get_contents( $file_path );

				if ( $file_contents ) {
					$json_data = json_decode( $file_contents, true );
					$file_type = $json_path === $file_path ? $import_file_type : explode( '.', str_replace( $directory . '/', '', $file_path ) )[0];

					if ( $collection_id && tutor()->course_post_type !== $file_type ) {
						continue;
					}

					$validate = ValidationHelper::validate( $json_validation_rules, $json_data );

					if ( ! $validate->success ) {
						throw new \Exception( __( 'Invalid file provided', 'tutor-pro' ) );
					}

					$file_data   = $json_data['data'] ?? array();
					$import_data = array();
					$requirement = array();

					$requirement['type'] = $file_type;

					if ( tutor()->course_post_type === $file_type ) {
						if ( ! isset( $updated_requirements[ tutor()->course_post_type ] ) ) {
							$requirement['ids'] = array( 1 );
						} else {
							$requirement['ids']   = $updated_requirements[ tutor()->course_post_type ]['ids'];
							$requirement['ids'][] = count( $requirement['ids'] ) + 1;
						}
					} elseif ( tutor()->bundle_post_type === $file_type ) {
						if ( ! isset( $updated_requirements[ tutor()->bundle_post_type ] ) ) {
							$requirement['ids'] = array( 1 );
						} else {
							$requirement['ids']   = $updated_requirements[ tutor()->bundle_post_type ]['ids'];
							$requirement['ids'][] = count( $requirement['ids'] ) + 1;
						}
					} elseif ( $this->exporter::TYPE_MEMBERSHIP_PLANS === $file_type ) {
						if ( ! isset( $updated_requirements[ $this->exporter::TYPE_MEMBERSHIP_PLANS ] ) ) {
							$requirement['ids'] = array( 1 );
						} else {
							$requirement['ids']   = $updated_requirements[ $this->exporter::TYPE_MEMBERSHIP_PLANS ]['ids'];
							$requirement['ids'][] = count( $requirement['ids'] ) + 1;
						}
					} elseif ( 'progress' === $file_type ) {
						if ( ! isset( $updated_requirements[ $file_type ] ) ) {
							$requirement['ids'] = array( 1 );
						} else {
							$requirement['ids']   = $updated_requirements[ $file_type ]['ids'];
							$requirement['ids'][] = count( $requirement['ids'] ) + 1;
						}
					} else {
						$import_data = $file_data[0]['data'][ $file_type ] ?? array();
						if ( ! $import_data ) {
							continue;
						}
						$requirement['ids'] = array_merge( $updated_requirements[ $file_type ]['ids'] ?? array(), array_keys( $import_data ) );
					}
				}

				$updated_requirements[ $file_type ] = $requirement;

				unset( $file_contents );

				if ( $json_path === $file_path ) {
					continue;
				} elseif ( Helper::TUTOR_COURSE_PLANS_TYPE === $file_type ) {
					// Plans should be before order.
					$order_json_path = $directory . '/' . Helper::TUTOR_COURSE_ORDERS_TYPE . '.json';
					if ( isset( $paths[ $order_json_path ] ) ) {
						unset( $paths[ $order_json_path ] );
						$paths[ $file_path ]       = $file_type;
						$paths[ $order_json_path ] = Helper::TUTOR_COURSE_ORDERS_TYPE;
					}
				} else {
					$paths[ $file_path ] = $file_type;
				}
			}
		}

		return array(
			'requirements' => $updated_requirements,
			'file_paths'   => $paths,
		);
	}

	/**
	 * Prepare job data for zip file import.
	 *
	 * @since 3.8.1
	 *
	 * @throws \Throwable If the files provided are invalid.
	 * @throws \Exception If the files provided are invalid.
	 *
	 * @param array  $file the zip import file.
	 * @param string $upload_dir the zip upload directory.
	 * @param int    $collection_id the collection id.
	 * @param int    $job_id the job id.
	 *
	 * @return array
	 */
	private function prepare_import_zip_job( $file, $upload_dir, $collection_id, $job_id ) {
		$file_paths       = array();
		$job_requirements = array();

		$json_validation_rules = array(
			'schema_version'   => 'required|match_string:2.0.0',
			'exported_at'      => 'required',
			'keep_media_files' => 'required',
			'keep_user_data'   => 'required',
			'data'             => 'required|is_array',
		);

		$zip_upload_path = $upload_dir;

		if ( ! file_exists( $upload_dir ) ) {
			$result = wp_mkdir_p( $upload_dir );

			// Set directory permissions (755 for security).
			if ( file_exists( $upload_dir ) ) {
				chmod( $upload_dir, 0755 );
			}
		}

		$result = unzip_file( $file['tmp_name'], $upload_dir );

		unlink( $file['tmp_name'] );

		if ( is_wp_error( $result ) ) {
			throw new \Exception( $result->get_error_message() );
		}

		if ( QueryHelper::table_exists( 'tutor_gradebooks' ) ) {
			$gradebook_settings_path = $zip_upload_path . '/' . $this->exporter::GRADE_BOOKS_SETTINGS . '.json';

			$gradebook_settings_content = file_exists( $gradebook_settings_path ) ? file_get_contents( $gradebook_settings_path ) : null;
		}

		if ( $gradebook_settings_content ) {
			$json_data = json_decode( $gradebook_settings_content, true );

			$validate = ValidationHelper::validate( $json_validation_rules, $json_data );

			if ( ! $validate->success ) {
				throw new \Exception( __( 'Gradebook Settings file provided is invalid', 'tutor-pro' ) );
			}

			$file_paths[ $gradebook_settings_path ] = $this->exporter::GRADE_BOOKS_SETTINGS;

			$job_requirements[ $this->exporter::GRADE_BOOKS_SETTINGS ] = array( 'type' => $this->exporter::GRADE_BOOKS_SETTINGS );

		}

		$courses_path = $zip_upload_path . '/courses';
		$courses      = glob( "$courses_path/*" ) ?? array();

		$bundle_path = $zip_upload_path . '/' . tutor()->bundle_post_type;

		$bundle_courses = glob( "$bundle_path/*/courses/*" );

		if ( $bundle_courses ) {
			if ( ! file_exists( $courses_path ) ) {
				wp_mkdir_p( $courses_path );

				if ( file_exists( $courses_path ) ) {
					chmod( $courses_path, 0755 );
				}
			}
			foreach ( $bundle_courses as $bundle_courses_path ) {
				$course_id_path     = preg_replace( '#^.*?/courses/(\d+)$#', '$1', $bundle_courses_path );
				$bundle_course_path = str_replace( $course_id_path, '', $bundle_courses_path );
				$final_course_path  = $courses_path . '/' . $course_id_path;

				// Move the courses files under the courses directory.
				$result = rename( $bundle_courses_path, $final_course_path );
				rmdir( $bundle_course_path );
				$courses[] = $final_course_path;
			}
		}

		if ( count( $courses ) ) {

			try {
				$result = $this->get_zip_import_job( $courses, $courses_path, $json_validation_rules, tutor()->course_post_type, $job_requirements, $file_paths, $collection_id );
			} catch ( \Throwable $th ) {
				throw $th;
			}

			$job_requirements = $result['requirements'];
			$file_paths       = $result['file_paths'];
		}

		$bundles = glob( "$bundle_path/*" ) ?? array();

		if ( count( $bundles ) && ! $collection_id ) {

			try {
				$result = $this->get_zip_import_job( $bundles, $bundle_path, $json_validation_rules, tutor()->bundle_post_type, $job_requirements, $file_paths );
			} catch ( \Throwable $th ) {
				throw $th;
			}

			$job_requirements = $result['requirements'];
			$file_paths       = $result['file_paths'];
		}

		$cb_collections = array();

		if ( tutor_utils()->is_addon_enabled( 'content-bank' ) ) {
			$collection_path = $zip_upload_path . '/' . $this->importer::TYPE_CONTENT_BANK;
			$cb_collections  = glob( "$collection_path/*" );
		}

		if ( count( $cb_collections ) ) {
			foreach ( $cb_collections as $cb_path ) {
				$cb_contents = file_get_contents( $cb_path );

				if ( $cb_contents ) {
					$json_data = json_decode( $cb_contents, true );

					$validate = ValidationHelper::validate( $json_validation_rules, $json_data );

					if ( ! $validate->success ) {
						continue;
					}

					$requirement = array();

					$requirement['type'] = $this->exporter::TYPE_CONTENT_BANK;

					if ( ! isset( $job_requirements[ $this->exporter::TYPE_CONTENT_BANK ] ) ) {
						$requirement['ids'] = array( 1 );
					} else {
						$requirement['ids']   = $job_requirements[ $this->exporter::TYPE_CONTENT_BANK ]['ids'];
						$requirement['ids'][] = count( $requirement['ids'] ) + 1;
					}

					$file_paths[ $cb_path ] = $this->exporter::TYPE_CONTENT_BANK;

				}

				$job_requirements[ $this->exporter::TYPE_CONTENT_BANK ] = $requirement;
			}
		}

		$membership_plans_path = $zip_upload_path . '/' . $this->exporter::TYPE_MEMBERSHIP_PLANS;
		$membership_plans      = glob( "$membership_plans_path/*" ) ?? array();

		if ( count( $membership_plans ) && ! $collection_id ) {
			try {
				$result = $this->get_zip_import_job( $membership_plans, $membership_plans_path, $json_validation_rules, $this->exporter::TYPE_MEMBERSHIP_PLANS, $job_requirements, $file_paths );
			} catch ( \Throwable $th ) {
				throw $th;
			}

			$job_requirements = $result['requirements'];
			$file_paths       = $result['file_paths'];
		}

		$settings_path = $zip_upload_path . '/' . $this->exporter::TYPE_SETTINGS . '.json';

		$settings_content = file_exists( $settings_path ) ? file_get_contents( $settings_path ) : null;

		if ( $settings_content ) {
			$json_data = json_decode( $settings_content, true );

			$validate = ValidationHelper::validate( $json_validation_rules, $json_data );

			if ( ! $validate->success ) {
				throw new \Exception( __( 'Settings file provided is invalid', 'tutor-pro' ) );
			}

			$file_paths[ $settings_path ] = $this->exporter::TYPE_SETTINGS;

			$job_requirements[ $this->exporter::TYPE_SETTINGS ] = array( 'type' => $this->exporter::TYPE_SETTINGS );

		}

		$job_data                    = $this->get_default_job_data( $job_id, $job_requirements );
		$job_data['collection_id']   = $collection_id ?? 0;
		$this->job_id                = $job_data['job_id'] ?? 0;
		$job_data['message']         = __( 'Import in progress...', 'tutor-pro' );
		$job_data['file_paths']      = $file_paths;
		$job_data['zip_upload_path'] = $zip_upload_path;

		return $job_data;
	}


	/**
	 * Handle import.
	 *
	 * @since 3.9.3
	 *
	 * @throws \Exception If file provided is invalid.
	 * @throws \Throwable If invalid file provided.
	 *
	 * @param int   $job_id the job id.
	 * @param int   $collection_id the collection id.
	 * @param array $file the $_FILE uploaded.
	 *
	 * @return array
	 */
	public function import_handler( $job_id = 0, $collection_id = 0, $file = null ) {

		global $wp_filesystem;

		// In some cases file system cannot be accessed.
		if ( ! $wp_filesystem ) {
			WP_Filesystem();
		}

		$job_data = $this->get_import_job( $job_id );

		$this->job_id    = $job_data['job_id'] ?? 0;
		$data            = $this->get_json_data( 'import' );
		$json_files_path = $job_data['file_paths'] ?? null;

		if ( $file ) {

			ContentMapHandler::clear_map();
			ErrorHandler::clear_errors();

			if ( 'application/zip' === $file['type'] ) {
				$upload_dir = wp_upload_dir()['basedir'] . '/tutor-pro/' . explode( '.', $file['name'] )[0];
				try {
					$job_data = $this->prepare_import_zip_job( $file, $upload_dir, $collection_id, $job_id );
				} catch ( \Throwable $th ) {
					Helper::remove_files_directory_recursively( $upload_dir );
					throw $th;
				}

				$this->update_import_job( $job_data );
				return $job_data;
			} else {
				try {
					$result   = $this->get_import_data_from_json( $file, $job_data, $collection_id );
					$data     = $result['data'];
					$job_data = $result['job_data'];
				} catch ( \Throwable $th ) {
					throw $th;
				}
			}
		}

		// Increase memory limit before import.
		Helper::increase_memory_limit();

		if ( $json_files_path ) {
			foreach ( $json_files_path as $file_path => $file_type ) {
				if ( ! $file_path ) {
					continue;
				}
				$import_data = file_get_contents( $file_path );
				$import_data = json_decode( $import_data, true );

				if ( $import_data ) {
					switch ( $file_type ) {
						case tutor()->course_post_type:
							$completed_count = count( $job_data['completed_contents'][ tutor()->course_post_type ]['success'] ) + count( $job_data['completed_contents'][ tutor()->course_post_type ]['failed'] );
							$course_data     = $import_data['data'][0]['data']['course'];
							$job_count       = count( $job_data['job_requirements'][ tutor()->course_post_type ]['ids'] );

							if ( $job_count !== $completed_count ) {
								$job_id    = $job_data['job_requirements'][ tutor()->course_post_type ]['ids'][ $completed_count ];
								$import_id = $this->importer->import_content( array( $course_data ), $import_data['keep_media_files'], $job_data['collection_id'] );

								if ( is_wp_error( $import_id ) ) {
									$job_data['completed_contents'][ tutor()->course_post_type ]['failed'][] = $course_data['ID'];
								} else {
									$job_data['completed_contents'][ tutor()->course_post_type ]['success'][] = $job_id;
								}

								unset( $job_data['file_paths'][ $file_path ] );
								unlink( $file_path );
								unset( $course_data );
								unset( $import_data );

								$this->update_import_job( $job_data );
								$job_data = $this->process_import_job();

								return $job_data;
							}
							break;
						case tutor()->bundle_post_type:
							$completed_count = count( $job_data['completed_contents'][ tutor()->bundle_post_type ]['success'] ) + count( $job_data['completed_contents'][ tutor()->bundle_post_type ]['failed'] );
							$bundle_data     = $import_data['data'][0]['data']['bundle'];
							$job_count       = count( $job_data['job_requirements'][ tutor()->bundle_post_type ]['ids'] );

							if ( $job_count !== $completed_count ) {
								$job_id    = $job_data['job_requirements'][ tutor()->bundle_post_type ]['ids'][ $completed_count ];
								$import_id = $this->importer->import_bundle( $bundle_data, $import_data['keep_media_files'], $collection_id );

								if ( is_wp_error( $import_id ) ) {
									$job_data['completed_contents'][ tutor()->bundle_post_type ]['failed'][] = $bundle_data['ID'];
								} else {
									$job_data['completed_contents'][ tutor()->bundle_post_type ]['success'][] = $job_id;
								}

								unset( $job_data['file_paths'][ $file_path ] );
								unlink( $file_path );
								unset( $bundle_data );
								unset( $import_data );

								$this->update_import_job( $job_data );
								$job_data = $this->process_import_job();

								return $job_data;
							}
							break;
						case $this->exporter::TYPE_MEMBERSHIP_PLANS:
							$completed_count = count( $job_data['completed_contents'][ $this->exporter::TYPE_MEMBERSHIP_PLANS ]['success'] ) + count( $job_data['completed_contents'][ $this->exporter::TYPE_MEMBERSHIP_PLANS ]['failed'] );
							$membership_data = $import_data['data'][0]['data']['plans'];
							$job_count       = count( $job_data['job_requirements'][ $this->exporter::TYPE_MEMBERSHIP_PLANS ]['ids'] );

							if ( $job_count !== $completed_count ) {
								$job_id    = $job_data['job_requirements'][ $this->exporter::TYPE_MEMBERSHIP_PLANS ]['ids'][ $completed_count ];
								$import_id = $this->importer->import_tutor_plan( $membership_data );

								if ( is_wp_error( $import_id ) ) {
									$job_data['completed_contents'][ $this->exporter::TYPE_MEMBERSHIP_PLANS ]['failed'][] = $membership_data['id'];
								} else {
									$job_data['completed_contents'][ $this->exporter::TYPE_MEMBERSHIP_PLANS ]['success'][] = $job_id;
								}

								unset( $job_data['file_paths'][ $file_path ] );
								unlink( $file_path );
								unset( $bundle_data );
								unset( $import_data );

								$this->update_import_job( $job_data );
								$job_data = $this->process_import_job();

								return $job_data;
							}
							break;
						case $this->exporter::TYPE_CONTENT_BANK:
							$completed_count = count( $job_data['completed_contents'][ $this->exporter::TYPE_CONTENT_BANK ]['success'] ) + count( $job_data['completed_contents'][ $this->exporter::TYPE_CONTENT_BANK ]['failed'] );
							$collection_data = $import_data['data'][0]['data'];
							$job_count       = count( $job_data['job_requirements'][ $this->exporter::TYPE_CONTENT_BANK ]['ids'] );

							if ( $job_count !== $completed_count ) {
								$job_id    = $job_data['job_requirements'][ $this->exporter::TYPE_CONTENT_BANK ]['ids'][ $completed_count ];
								$import_id = $this->importer->import_content( $collection_data, $import_data['keep_media_files'], $collection_id );

								if ( is_wp_error( $import_id ) ) {
									$job_data['completed_contents'][ $this->exporter::TYPE_CONTENT_BANK ]['failed'][] = $collection_data['ID'];
								} else {
									$job_data['completed_contents'][ $this->exporter::TYPE_CONTENT_BANK ]['success'][] = $job_id;
								}

								unset( $job_data['file_paths'][ $file_path ] );
								unlink( $file_path );
								unset( $course_data );
								unset( $import_data );

								$this->update_import_job( $job_data );
								$job_data = $this->process_import_job();

								return $job_data;
							}
							break;
						case Helper::TUTOR_COURSE_PROGRESS_TYPE:
							$completed_count = count( $job_data['completed_contents'][ $file_type ]['success'] );
							$progress_data   = $import_data['data'][0]['data'][ $file_type ];
							$job_count       = count( $job_data['job_requirements'][ $file_type ]['ids'] );

							if ( $job_count !== $completed_count ) {
								$job_id = $job_data['job_requirements'][ $file_type ]['ids'][ $completed_count ];

								$this->importer->import_tutor_course_progress( $progress_data );

								$job_data['completed_contents'][ $file_type ]['success'][] = $job_id;

								unset( $job_data['file_paths'][ $file_path ] );
								unlink( $file_path );
								unset( $course_data );
								unset( $import_data );

								$this->update_import_job( $job_data );
								$job_data = $this->process_import_job();

								return $job_data;
							}
							break;
						case Helper::TUTOR_COURSE_ENROLLMENTS_TYPE:
							$completed_count = count( $job_data['completed_contents'][ $file_type ]['success'] ) + count( $job_data['completed_contents'][ $file_type ]['failed'] );
							$import_job_data = $import_data['data'][0]['data'][ $file_type ];
							$job_count       = count( $job_data['job_requirements'][ $file_type ]['ids'] );

							if ( $job_count !== $completed_count ) {
								$job_id = $job_data['job_requirements'][ $file_type ]['ids'][ $completed_count ];

								$import_id = $this->importer->import_tutor_enrollments( $import_job_data[ $job_id ] );

								if ( is_wp_error( $import_id ) ) {
									$job_data['completed_contents'][ $file_type ]['failed'][] = $import_job_data[ $job_id ]['enrollment']['ID'] ?? 1;
								} else {
									$job_data['completed_contents'][ $file_type ]['success'][] = $completed_count;
								}

								if ( count( $import_job_data ) - 1 === $job_id ) {
									unset( $job_data['file_paths'][ $file_path ] );
									unset( $import_job_data );
									unlink( $file_path );
								}
								$this->update_import_job( $job_data );
								$job_data = $this->process_import_job();

								return $job_data;
							}
							break;
						case Helper::TUTOR_COURSE_ORDERS_TYPE:
							$completed_count = count( $job_data['completed_contents'][ $file_type ]['success'] ) + count( $job_data['completed_contents'][ $file_type ]['failed'] );
							$import_job_data = $import_data['data'][0]['data'][ $file_type ];
							$job_count       = count( $job_data['job_requirements'][ $file_type ]['ids'] );

							if ( $job_count !== $completed_count ) {
								$job_id = $job_data['job_requirements'][ $file_type ]['ids'][ $completed_count ];

								$import_id = $this->importer->import_tutor_orders( $import_job_data[ $job_id ] );

								if ( is_wp_error( $import_id ) ) {
									$job_data['completed_contents'][ $file_type ]['failed'][] = $import_job_data[ $job_id ]['orders']['id'];
								} else {
									$job_data['completed_contents'][ $file_type ]['success'][] = $completed_count;
								}

								if ( count( $import_job_data ) - 1 === $job_id ) {
									unset( $job_data['file_paths'][ $file_path ] );
									unset( $import_job_data );
									unlink( $file_path );
								}
								$this->update_import_job( $job_data );
								$job_data = $this->process_import_job();

								return $job_data;
							}
							break;
						case Helper::TUTOR_COURSE_SUBSCRIPTION_TYPE:
							$completed_count = count( $job_data['completed_contents'][ $file_type ]['success'] ) + count( $job_data['completed_contents'][ $file_type ]['failed'] );
							$import_job_data = $import_data['data'][0]['data'][ $file_type ];
							$job_count       = count( $job_data['job_requirements'][ $file_type ]['ids'] );

							if ( $job_count !== $completed_count ) {
								$job_id = $job_data['job_requirements'][ $file_type ]['ids'][ $completed_count ];

								$import_id = $this->importer->import_tutor_subscriptions( $import_job_data[ $job_id ] );

								if ( is_wp_error( $import_id ) ) {
									$job_data['completed_contents'][ $file_type ]['failed'][] = $import_job_data[ $job_id ]['id'];
								} else {
									$job_data['completed_contents'][ $file_type ]['success'][] = $completed_count;
								}

								if ( count( $import_job_data ) - 1 === $job_id ) {
									unset( $job_data['file_paths'][ $file_path ] );
									unset( $import_job_data );
									unlink( $file_path );
								}
								$this->update_import_job( $job_data );
								$job_data = $this->process_import_job();

								return $job_data;
							}
							break;
						case Helper::TUTOR_COURSE_PLANS_TYPE:
							$completed_count = count( $job_data['completed_contents'][ $file_type ]['success'] ) + count( $job_data['completed_contents'][ $file_type ]['failed'] );
							$import_job_data = $import_data['data'][0]['data'][ $file_type ];
							$job_count       = count( $job_data['job_requirements'][ $file_type ]['ids'] );

							if ( $job_count !== $completed_count ) {
								$job_id = $job_data['job_requirements'][ $file_type ]['ids'][ $completed_count ];

								$import_id = $this->importer->import_tutor_plan( $import_job_data[ $job_id ] );

								if ( is_wp_error( $import_id ) ) {
									$job_data['completed_contents'][ $file_type ]['failed'][] = $import_job_data[ $job_id ]['id'];
								} else {
									$job_data['completed_contents'][ $file_type ]['success'][] = $completed_count;
								}

								if ( count( $import_job_data ) - 1 === $job_id ) {
									unset( $job_data['file_paths'][ $file_path ] );
									unset( $import_job_data );
									unlink( $file_path );
								}

								$this->update_import_job( $job_data );
								$job_data = $this->process_import_job();

								return $job_data;
							}
							break;
						case Helper::TUTOR_COURSE_REVIEW_TYPE:
							$completed_count = count( $job_data['completed_contents'][ $file_type ]['success'] ) + count( $job_data['completed_contents'][ $file_type ]['failed'] );
							$import_job_data = $import_data['data'][0]['data'][ $file_type ];
							$job_count       = count( $job_data['job_requirements'][ $file_type ]['ids'] );

							if ( $job_count !== $completed_count ) {
								$job_id = $job_data['job_requirements'][ $file_type ]['ids'][ $completed_count ];

								$import_id = $this->importer->import_tutor_reviews( $import_job_data[ $job_id ] );

								if ( is_wp_error( $import_id ) ) {
									$job_data['completed_contents'][ $file_type ]['failed'][] = $import_job_data[ $job_id ]['review']['comment_ID'];
								} else {
									$job_data['completed_contents'][ $file_type ]['success'][] = $completed_count;
								}

								if ( count( $import_job_data ) - 1 === $job_id ) {
									unset( $job_data['file_paths'][ $file_path ] );
									unset( $import_job_data );
									unlink( $file_path );
								}
								$this->update_import_job( $job_data );
								$job_data = $this->process_import_job();

								return $job_data;
							}
							break;
						case $this->exporter::TYPE_SETTINGS:
							$import_job_data = $import_data['data'][0]['data'];
							$response        = $this->importer->import_settings( $import_job_data );

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

							$job_data['completed_contents'][ $file_type ] = $response;

							unset( $job_data['file_paths'][ $file_path ] );
							unset( $import_job_data );
							unlink( $file_path );

							$this->update_import_job( $job_data );
							$job_data = $this->process_import_job();

							return $job_data;
						case $this->exporter::GRADE_BOOKS_SETTINGS:
							$import_job_data = $import_data['data'][0]['data'];
							$response        = $this->importer->import_grade_settings( $import_job_data );

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

							$job_data['completed_contents'][ $file_type ] = $response;

							unset( $job_data['file_paths'][ $file_path ] );
							unset( $import_job_data );
							unlink( $file_path );

							$this->update_import_job( $job_data );
							$job_data = $this->process_import_job();

							return $job_data;
						default:
							break;

					}

					$progress = (int) $this->calculate_progress( $job_data );

					if ( 100 === $progress ) {
						$job_data = $this->process_import_job();
						return $job_data;
					}
				}
			}
		} elseif ( $data ) {
			foreach ( $data as $content ) {
				$content_type = $content['content_type'];

				$ids = array();

				if ( $this->importer::TYPE_CONTENT_BANK === $content_type ) {
					$content_type = $this->exporter::TYPE_CONTENT_BANK;
				}

				if ( $this->exporter::TYPE_SETTINGS !== $content_type ) {
					$completed_contents = array_merge( $job_data['completed_contents'][ $content_type ]['success'], $job_data['completed_contents'][ $content_type ]['failed'] );
					$ids                = array_diff( $job_data['job_requirements'][ $content_type ]['ids'], $completed_contents );
				}

				switch ( $content['content_type'] ) {
					case get_tutor_post_types( 'course' ):
						if ( $ids ) {
							$id = array_shift( $ids );
							foreach ( $content['data'] as $data ) {
								if ( $data['ID'] !== $id ) {
									continue;
								}
								$this->update_import_job( $job_data, $data, $content_type, $id );
								$job_data = $this->process_import_job();

								return $job_data;
							}
						}
						break;
					case $this->importer::TYPE_CONTENT_BANK:
						if ( $ids && tutor_utils()->is_addon_enabled( 'content-bank' ) ) {
							$id = array_shift( $ids );
							foreach ( $content['data'] as $data ) {
								if ( $data['ID'] !== $id ) {
									continue;
								}
								$this->update_import_job( $job_data, $data, $content_type, $id );
								$job_data = $this->process_import_job();

								return $job_data;
							}
						}
						break;
					case get_tutor_post_types( 'bundle' ):
						if ( $ids ) {
							$id = array_shift( $ids );
							foreach ( $content['data'] as $data ) {
								if ( $data['ID'] !== $id ) {
									continue;
								}
								$this->update_import_job( $job_data, $data, $content_type, $id );
								$job_data = $this->process_import_job();

								return $job_data;
							}
						}
						break;
					case $this->exporter::TYPE_SETTINGS:
						$this->update_import_job( $job_data, $content['data'], $content_type );
						$job_data = $this->process_import_job();

						return $job_data;
					default:
						break;
				}
			}

			$progress = (int) $this->calculate_progress( $job_data );

			if ( 100 === $progress ) {
				$job_data = $this->process_import_job();
				return $job_data;
			}
		} else {
			throw new \Exception( __( 'Invalid or empty data provided', 'tutor-pro' ) );
		}
	}

	/**
	 * Handle ajax import.
	 *
	 * @since 3.6.0
	 *
	 * @return void wp_json response
	 */
	public function ajax_import_handler() {
		tutor_utils()->check_nonce();
		tutor_utils()->check_current_user_capability();

		$job_id        = Input::post( 'job_id', 0 );
		$collection_id = Input::post( 'collection_id', 0 );
		$file 		   = $_FILES['data'] ?? ''; // phpcs:ignore
		try {
			$result       = $this->import_handler( $job_id, $collection_id, $file );
			$job_progress = (int) $result['job_progress'];
			if ( 100 === $job_progress ) {
				$this->json_response( __( 'Import completed', 'tutor-pro' ), $result );
			} else {
				$this->json_response( '', $result );
			}
		} catch ( \Throwable $th ) {
			$this->response_bad_request( $th->getMessage() );
		}
	}

	/**
	 * Prepare initial contents for import.
	 *
	 * @since 3.7.1
	 *
	 * @throws \Throwable If content cannot be saved.
	 *
	 * @param array $contents the contents obtained from file.
	 * @param int   $collection_id the collection id.
	 *
	 * @return array|\Throwable
	 */
	private function prepare_import_contents( $contents, $collection_id = 0 ) {
		$keep_media_files = false;

		if ( isset( $contents['keep_media_files'] ) ) {
			$keep_media_files = $contents['keep_media_files'];
		}

		$data = $contents['data'];

		$rules = array(
			'schema_version' => 'required',
			'data'           => 'required|is_array',
		);

		$validate_json = ValidationHelper::validate( $rules, $contents );

		if ( ! $validate_json->success ) {
			return new \Exception( 'invalid_json', __( 'Invalid json', 'tutor-pro' ) );
		}

		$result = $this->prepare_import_job_contents( $data, $collection_id );

		if ( is_wp_error( $result ) ) {
			return new \Exception( $result->get_error_message() );
		}

		$job_data = $this->get_default_job_data( $this->job_id, $result['job_requirement'] );

		$this->job_id = $job_data['job_id'];

		$job_data['keep_media_files'] = $keep_media_files;
		$job_data['message']          = __( 'Import in progress...', 'tutor-pro' );

		if ( $collection_id ) {
			$job_data['collection_id'] = $collection_id;
		}

		try {
			$this->save_import_json_content( $result['updated_data'] );
		} catch ( \Throwable $th ) {
			return $th;
		}

		return $job_data;
	}

	/**
	 * Read contents from import file.
	 *
	 * @since 3.7.0
	 *
	 * @param array $file the import file data.
	 * @param array $job_data the job data.
	 *
	 * @return string|\WP_Error
	 */
	private function read_import_file( $file, $job_data ) {
		if ( ! $file && ! $job_data ) {
			return new \WP_Error( 'empty_file', __( 'Invalid or empty file provided', 'tutor-pro' ) );
		}

		if ( $file ) {
			if ( UPLOAD_ERR_OK !== $file['error'] ) {
				return new \WP_Error( 'upload_error', __( 'File upload error', 'tutor-pro' ) );
			}

			try {
				$contents = file_get_contents( $file['tmp_name'] );

				if ( ! $contents ) {
					return new \WP_Error( 'error_file_reading', __( 'Error reading from file', 'tutor-pro' ) );
				}
			} catch ( \Throwable $th ) {
				$this->response_bad_request( $th->getMessage() );
			} finally {
				unlink( $file['tmp_name'] );
			}
		}

		return $contents;
	}

	/**
	 * Prepare Import Job Requirements.
	 *
	 * @since 3.7.0
	 *
	 * @param array $data the contents.
	 * @param int   $collection_id the collection id.
	 *
	 * @return array|\WP_Error
	 */
	private function prepare_import_job_contents( $data, $collection_id = 0 ) {

		$job_requirements = array();

		$importable_contents = array();

		foreach ( $this->exporter->get_exportable_content() as $value ) {
			if ( $this->exporter::TYPE_CONTENT_BANK === $value['key'] ) {
				$importable_contents[] = $this->importer::TYPE_CONTENT_BANK;
				continue;
			}

			$importable_contents[] = $value['key'];
		}

		$updated_data = $data;

		foreach ( $data as $key => $content ) {
			$requirement    = array();
			$bundle_courses = array();
			$rules          = array(
				'content_type' => 'required|match_string:' . implode( ',', $importable_contents ),
				'data'         => 'required|is_array',
			);

			$validate_content = ValidationHelper::validate( $rules, $content );

			if ( ! $validate_content->success ) {
				return new \WP_Error( 'invalid_data', __( 'The data provided is invalid', 'tutor-pro' ), $validate_content->errors );
			}

			if ( $this->importer::TYPE_CONTENT_BANK === $content['content_type'] && ! tutor_utils()->is_addon_enabled( 'content-bank' ) ) {
				unset( $updated_data[ $key ] );
				continue;
			}

			if ( get_tutor_post_types( 'bundle' ) === $content['content_type'] ) {
				$course_ids  = $job_requirements[ get_tutor_post_types( 'course' ) ]['ids'] ?? array();
				$course_data = array(
					'content_type' => get_tutor_post_types( 'course' ),
					'data'         => array(),
				);

				if ( $course_ids ) {
					$course_data = array_shift( $updated_data );
				}
				foreach ( $content['data'] as $bundle ) {
					$bundle_courses = $bundle['courses'];

					if ( ! count( $bundle_courses ) ) {
						continue;
					}

					$bundle_course_ids = array_column( $bundle_courses, 'ID' );

					if ( count( $course_ids ) ) {
						$unique_ids = array_diff( $bundle_course_ids, $course_ids );
						array_push( $job_requirements[ get_tutor_post_types( 'course' ) ]['ids'], ...$unique_ids );

						$course_data['data'] = array_merge( $course_data['data'], $bundle_courses );

					} else {
						if ( $job_requirements[ get_tutor_post_types( 'course' ) ] ) {
							$unique_ids = array_diff( $bundle_course_ids, $job_requirements[ get_tutor_post_types( 'course' ) ]['ids'] );
							array_push( $job_requirements[ get_tutor_post_types( 'course' ) ]['ids'], ...$unique_ids );
						} else {
							$requirement['type']                                  = get_tutor_post_types( 'course' );
							$requirement['ids']                                   = $bundle_course_ids;
							$job_requirements[ get_tutor_post_types( 'course' ) ] = $requirement;
						}

						$course_data['data'] = array_merge( $course_data['data'], $bundle_courses );
					}
				}

				array_unshift( $updated_data, $course_data );

				if ( $collection_id ) {
					continue;
				}
			}

			$requirement['type'] = $this->importer::TYPE_CONTENT_BANK === $content['content_type'] ? $this->exporter::TYPE_CONTENT_BANK : $content['content_type'];

			if ( $this->exporter::TYPE_SETTINGS !== $content['content_type'] ) {
				$requirement['ids'] = array_column( $content['data'], 'ID' );
			}

			$job_requirements[ $requirement['type'] ] = $requirement;
		}

		if ( $collection_id ) {
			foreach ( $updated_data as $key => $data ) {
				if ( get_tutor_post_types( 'bundle' ) === $data['content_type'] ) {
					unset( $updated_data[ $key ] );
				}
			}
		}

		return array(
			'job_requirement' => $job_requirements,
			'updated_data'    => $updated_data,
		);
	}

	/**
	 * Save import content in json file.
	 *
	 * @param array $content the content to store.
	 *
	 * @throws \Exception If error writing import content to file.
	 *
	 * @return int|bool
	 */
	private function save_import_json_content( $content ) {
		$json_file = $this->get_json_file( 'import' );

		$data = file_put_contents( $json_file, wp_json_encode( $content, JSON_PRETTY_PRINT ) );

		if ( ! $data ) {
			throw new \Exception( esc_html__( 'Error writing to file', 'tutor-pro' ) );
		}

		return $data;
	}


	/**
	 * Get job data
	 *
	 * @since 3.6.0
	 *
	 * @param mixed $job_id Job id to get job data.
	 *
	 * @return array
	 */
	private function get_export_job( $job_id ) {
		return get_option( $this->exporter::OPT_NAME . $job_id, null );
	}

	/**
	 * Get import job data
	 *
	 * @since 3.6.0
	 *
	 * @param mixed $job_id Job id to get job data.
	 *
	 * @return array
	 */
	private function get_import_job( $job_id ) {
		return get_option( $this->importer::OPT_NAME . $job_id, null );
	}

	/**
	 * Update export job data
	 *
	 * @since 3.6.0
	 *
	 * @throws \Throwable If failed to update the json file.
	 *
	 * @param array  $job_data Job data.
	 * @param string $job_type New done job type.
	 * @param mixed  $new_export_data Exported data to merge with the job data.
	 *
	 * @return void
	 */
	private function update_export_job( array $job_data, string $job_type, $new_export_data ) {

		if ( empty( $new_export_data ) ) {
			update_option( $this->exporter::OPT_NAME . $this->job_id, $job_data, false );
		} else {
			$this->content_type                 = $new_export_data['data'][0]['content_type'] ?? $job_type;
			$this->content_data                 = $new_export_data['data'][0]['data'] ?? array();
			$this->current_processing_parent_id = $job_data['completed_contents'][ $this->content_type ]['current_processing_id'] ?? null;

			try {
				$this->get_json_file_data( $new_export_data );
			} catch ( \Throwable $th ) {
				tutor_log( 'update export ' . $th->getMessage() );
			}

			$job_data['job_progress'] = $this->calculate_progress( $job_data );

			update_option( $this->exporter::OPT_NAME . $this->job_id, $job_data, false );
		}
	}

	/**
	 * Update import job data
	 *
	 * @since 3.6.0
	 * @since 3.7.1 param $data & $content_type & $content_id added.
	 *
	 * @param array  $job_data     the job data to update.
	 * @param array  $data         the array of content data.
	 * @param string $content_type the type of content being imported.
	 * @param int    $content_id   the content id being imported.
	 *
	 * @return void|\WP_Error
	 */
	private function update_import_job( array $job_data, array $data = array(), string $content_type = '', int $content_id = 0 ) {
		if ( $data ) {
			$import_id = null;

			if ( get_tutor_post_types( 'course' ) === $content_type ) {
				$import_id = $this->importer->import_content( array( $data ), $job_data['keep_media_files'], $job_data['collection_id'] ?? 0 );
			}

			if ( $this->exporter::TYPE_CONTENT_BANK === $content_type ) {
				$import_id = $this->importer->import_content( array( $data ), $job_data['keep_media_files'] );
			}

			if ( get_tutor_post_types( 'bundle' ) === $content_type ) {
				$import_id = $this->importer->import_bundle( $data, $job_data['keep_media_files'] );
			}

			if ( $this->exporter::TYPE_SETTINGS === $content_type ) {
				$response = $this->importer->import_settings( $data );

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

				$job_data['completed_contents'][ $content_type ] = $response;
			} else {
				if ( is_wp_error( $import_id ) ) {
					$job_data['completed_contents'][ $content_type ]['failed'][] = $content_id;
				}

				if ( isset( $import_id ) && ! is_wp_error( $import_id ) ) {
					$job_data['completed_contents'][ $content_type ]['success'][] = $content_id;
				}
			}
		}
		update_option( $this->importer::OPT_NAME . $this->job_id, $job_data, false );
	}

	/**
	 * Prepare and send job response
	 *
	 * @since 3.6.0
	 *
	 * @param string $message Response message.
	 * @param int    $status_code Status code.
	 *
	 * @return void
	 */
	private function send_export_response( string $message, $status_code = HttpHelper::STATUS_OK ) {
		$job_data = $this->get_export_job( $this->job_id );
		$progress = (int) $this->calculate_progress( $job_data );

		$response_to_client = null;
		if ( 100 === $progress ) {
			$job_data['job_progress'] = $progress;
			$job_data['job_status']   = 'completed';

			$job_data = $this->generate_progress_message( $job_data, $progress, false );

			$source_dir = $this->upload_dir . "export-{$this->job_id}";
			$zip_path   = trailingslashit( $this->upload_dir ) . 'tutor-lms-data-' . gmdate( 'Y-m-d-H-i-s', tutor_time() ) . '.zip';

			// Create zip and save the zip file name in the job data.
			$job_data['exported_data'] = basename( FileSystemHelper::zip( $source_dir, $zip_path ) );

			// Download url and file size.
			$job_data['export_file'] = array(
				'url'       => Helper::get_download_url( basename( $zip_path ) ),
				'file_size' => size_format( filesize( $zip_path ) ),
			);

			do_action( 'tutor_pro_export_completed', $this->job_id, $job_data['exported_data'] );

			// Send response to the client.
			$response_to_client = $job_data;

			// Unlink the exported folder.
			if ( is_dir( $source_dir ) ) {
				FileSystemHelper::delete_directory( $source_dir );
			}
		} else {
			$job_data['job_progress'] = $progress;
			$job_data                 = $this->generate_progress_message( $job_data, $progress, false );
		}

		update_option( $this->exporter::OPT_NAME . $this->job_id, $job_data, false );

		$this->json_response( $job_data['message'], $response_to_client ? $response_to_client : $job_data, $status_code );
	}

	/**
	 * Prepare import job before sending response
	 *
	 * @since 3.6.0
	 *
	 * @return array
	 */
	private function process_import_job() {
		$job_data = $this->get_import_job( $this->job_id );
		$progress = (int) $this->calculate_progress( $job_data );

		if ( 100 === $progress ) {
			$job_data['job_progress'] = $progress;
			$job_data['job_status']   = 'completed';

			if ( ErrorHandler::get_errors() ) {
				$job_data['errors'] = ErrorHandler::get_errors();
				ErrorHandler::clear_errors();
			}

			ContentMapHandler::clear_map();

			if ( isset( $job_data['zip_upload_path'] ) ) {
				Helper::remove_files_directory_recursively( $job_data['zip_upload_path'] );
				unset( $job_data['zip_upload_path'] );
			}

			$job_data = $this->generate_progress_message( $job_data, $progress, true );

			// Unlink the json file.
			if ( file_exists( $this->get_json_file( 'import' ) ) ) {
				unlink( $this->get_json_file( 'import' ) );
			}
		} else {
			$job_data['job_progress'] = $progress;
			$job_data                 = $this->generate_progress_message( $job_data, $progress, true );
		}

		$this->update_import_job( $job_data );

		return $job_data;
	}

	/**
	 * Get progress message title and label by type.
	 *
	 * @since 3.8.1
	 *
	 * @param string  $type the type of content.
	 * @param integer $progress the progress count.
	 * @param integer $count the count of items.
	 *
	 * @return array|string
	 */
	private function get_progress_label_by_type( string $type, int $progress, int $count ) {
		switch ( $type ) {
			case tutor()->course_post_type:
				return 100 === $progress ? array(
					// translators: %d : number of course imported/exported successfully.
					sprintf( _n( ' Course (%d)', ' Courses (%d)', $count, 'tutor-pro' ), $count ),
					// translators: %d : number of course imported/exported successfully.
					sprintf( _n( ' Course ID (%d)', ' Course IDs (%d)', $count, 'tutor-pro' ), $count ),
				) :
				// translators: %d : number of course imported/exported successfully.
				sprintf( _n( ' %d Course', ' %d Courses', $count, 'tutor-pro' ), $count );
			case tutor()->bundle_post_type:
				return 100 === $progress ? array(
					// translators: %d : number of bundles imported/exported successfully.
					sprintf( _n( ' Bundle (%d)', ' Bundles (%d)', $count, 'tutor-pro' ), $count ),
					// translators: %d : number of bundles imported/exported successfully.
					sprintf( _n( ' Bundle ID (%d)', ' Bundle IDs (%d)', $count, 'tutor-pro' ), $count ),
				) :
				// translators: %d : number of bundles imported/exported successfully.
				sprintf( _n( ' %d Bundle', ' %d Bundles', $count, 'tutor-pro' ), $count );
			case $this->exporter::TYPE_CONTENT_BANK:
				return 100 === $progress ? array(
					// translators: %d : number of collections imported/exported successfully.
					sprintf( _n( ' Collection (%d)', ' Collections (%d)', $count, 'tutor-pro' ), $count ),
					// translators: %d : number of collections imported/exported successfully.
					sprintf( _n( ' Collection ID (%d)', ' Collection IDs (%d)', $count, 'tutor-pro' ), $count ),
				) :
				// translators: %d : number of collections imported/exported successfully.
				sprintf( _n( ' %d Collection', ' %d Collections', $count, 'tutor-pro' ), $count );
			case $this->exporter::TYPE_MEMBERSHIP_PLANS:
				return 100 === $progress ? array(
					// translators: %d : number of plans imported/exported successfully.
					sprintf( _n( ' Membership Plan (%d)', ' Membership Plans (%d)', $count, 'tutor-pro' ), $count ),
					// translators: %d : number of plans imported/exported successfully.
					sprintf( _n( ' Membership Plan ID (%d)', ' Membership Plan IDs (%d)', $count, 'tutor-pro' ), $count ),
				) :
				// translators: %d : number of plans imported/exported successfully.
				sprintf( _n( ' %d Membership Plan', ' %d Membership Plans', $count, 'tutor-pro' ), $count );
			case Helper::TUTOR_COURSE_ENROLLMENTS_TYPE:
				return sprintf(
					// translators: %d : number of enrollments imported/exported successfully.
					_n( 'Enrollment ID (%d)', 'Enrollment IDs (%d)', $count, 'tutor-pro' ),
					$count
				);
			case Helper::TUTOR_COURSE_REVIEW_TYPE:
				return sprintf(
					// translators: %d : number of reviews imported/exported successfully.
					_n( 'Review ID (%d)', 'Review IDs (%d)', $count, 'tutor-pro' ),
					$count
				);
			case Helper::TUTOR_COURSE_ORDERS_TYPE:
				return sprintf(
					// translators: %d : number of orders imported/exported successfully.
					_n( 'Order ID (%d)', 'Order IDs (%d)', $count, 'tutor-pro' ),
					$count
				);
			case Helper::TUTOR_COURSE_PLANS_TYPE:
				return sprintf(
					// translators: %d : number of subscription plans imported/exported successfully.
					_n( 'Subscription Plan ID (%d)', 'Subscription Plan IDs (%d)', $count, 'tutor-pro' ),
					$count
				);
		}
	}

	/**
	 * Generate import/export progress message dynamically.
	 *
	 * @since 3.8.1
	 *
	 * @param array   $job_data the job data.
	 * @param integer $progress the import/export progress.
	 * @param boolean $is_import whether it is import or export.
	 *
	 * @return array
	 */
	private function generate_progress_message( array $job_data, int $progress, bool $is_import ) {
		$updated_job_data = $job_data;

		$not_user_data = array(
			tutor()->course_post_type,
			tutor()->bundle_post_type,
			$this->exporter::TYPE_CONTENT_BANK,
			$this->exporter::TYPE_MEMBERSHIP_PLANS,
		);

		$progress_type    = $is_import ? __( 'Importing', 'tutor-pro' ) : __( 'Exporting', 'tutor-pro' );
		$has_user_data    = false;
		$failed_user_data = false;
		$has_settings     = false;
		$message          = '';
		$failed_message   = '';

		foreach ( $updated_job_data['completed_contents'] as $type => $content ) {

			if ( $type === $this->exporter::TYPE_SETTINGS ) {
				if ( ! $content ) {
					unset( $updated_job_data['completed_contents'][ $type ] );
				} else {
					$has_settings = true;
				}
				continue;
			}

			if ( $type === $this->exporter::GRADE_BOOKS_SETTINGS ) {
				if ( ! $content ) {
					unset( $updated_job_data['completed_contents'][ $type ] );
				} else {
					$has_user_data = true;
				}
				continue;
			}

			$success = count( $content['success'] );
			$failed  = count( $content['failed'] );

			if ( ! in_array( $type, $not_user_data ) ) {
				if ( $success ) {
					$has_user_data = true;
				}
				if ( $failed && 100 === $progress ) {
					$failed_user_data = true;
					$updated_job_data['completed_contents'][ $type ]['label'] = $this->get_progress_label_by_type( $type, $progress, $failed ) . ',' ?? '';
				}
			} else {

				if ( $success ) {
					$message .= 100 === $progress ? $this->get_progress_label_by_type( $type, $progress, $success )[0] . ',' : $this->get_progress_label_by_type( $type, $progress, $success ) . ',';
				}

				if ( $failed ) {
					$failed_message .= 100 === $progress ? $this->get_progress_label_by_type( $type, $progress, $failed )[0] . ',' : $this->get_progress_label_by_type( $type, $progress, $failed ) . ',';
					$updated_job_data['completed_contents'][ $type ]['label'] = $this->get_progress_label_by_type( $type, $progress, $failed )[1] . ',' ?? '';
				}
			}
		}

		if ( $failed_user_data ) {
			$failed_message .= __( ' User Data,', 'tutor-pro' );
		}

		if ( $has_user_data ) {
			$message .= __( ' User Data,', 'tutor-pro' );
		}

		if ( $has_settings ) {
			$message .= __( ' Settings,', 'tutor-pro' );
		}

		if ( 100 === $progress ) {
			$updated_job_data['failed_message'] = rtrim( trim( $failed_message ), ',' );
			$updated_job_data['message']        = rtrim( trim( $message ), ',' );
		} else {
			$message = rtrim( trim( $message ), ',' ) . ' ' . $progress_type;

			if ( $failed_message ) {
				$message .= ' and ' . rtrim( trim( $failed_message ), ',' ) . __( ' Failed', 'tutor-pro' );
			}
			$updated_job_data['message'] = $message;
		}

		return $updated_job_data;
	}

	/**
	 * Calculate the job progress based on the data
	 *
	 * @since 3.6.0
	 *
	 * @param array $job_data Job data.
	 *
	 * @return number Job progress
	 */
	private function calculate_progress( array $job_data ) {
		$total     = 0;
		$completed = 0;

		foreach ( $job_data['job_requirements'] as $content ) {

			if ( in_array( $content['type'], Exporter::SETTINGS_CONTENT_TYPES ) ) {

				++$total;

				if ( ! empty( $job_data['completed_contents'][ $content['type'] ] ) ) {
					++$completed;
				}
				continue;
			}

			$total += count( $content['ids'] );

			$completed_content_type = $job_data['completed_contents'][ $content['type'] ];

			if ( is_array( $completed_content_type ) ) {

				$completed += count( $completed_content_type['success'] );
				$completed += count( $completed_content_type['failed'] );
			}
		}

		return $total > 0 ? round( ( $completed / $total ) * 100, 2 ) : 100;
	}

	/**
	 * Get the JSON file
	 *
	 * @since 3.6.0
	 *
	 * @since 3.8.1 added \WP_Filesystem_Base.
	 *
	 * @global \WP_Filesystem_Base $wp_filesystem WordPress filesystem instance.
	 *
	 * @param string $type whether import or export.
	 *
	 * @return string JSON file path
	 */
	private function get_json_file( string $type = 'export' ) {

		global $wp_filesystem;

		if ( 'import' === $type ) {
			$import_file = $this->upload_dir . 'import-' . $this->job_id . '.json';

			// Ensure directory exists with proper permissions.
			if ( ! file_exists( $this->upload_dir ) ) {
				wp_mkdir_p( $this->upload_dir );

				// Set directory permissions (755 for security).
				if ( file_exists( $this->upload_dir ) ) {
					chmod( $this->upload_dir, 0755 );
				}
			}

			return $import_file;
		}

		list( $folder, $file_name ) = $this->determine_export_file_name();

		if ( empty( $file_name ) ) {
			return '';
		}

		$export_folder = $this->upload_dir . "{$type}-{$this->job_id}/{$this->content_type}/{$folder}";

		// Ensure directory exists with proper permissions.
		if ( ! $wp_filesystem->is_dir( $export_folder ) ) {
			wp_mkdir_p( $export_folder );

			if ( $wp_filesystem->is_dir( $export_folder ) ) {
				chmod( $export_folder, 0755 ); //phpcs:ignore
			}
		}

		return trailingslashit( $export_folder ) . $file_name . '.json';
	}

	/**
	 * Get the JSON file
	 *
	 * @since 3.6.0
	 *
	 * @since 3.8.1 added \WP_Filesystem_Base.
	 *
	 * @global \WP_Filesystem_Base $wp_filesystem WordPress filesystem instance.
	 *
	 * @param string $type whether import or export.
	 *
	 * @return array JSON file path
	 */
	private function get_json_data( string $type = 'export' ) {

		global $wp_filesystem;

		$export_file = $this->get_json_file( $type );
		$data        = $wp_filesystem->get_contents( $export_file );

		return json_decode( $data, true );
	}

	/**
	 * Update json file update
	 *
	 * @since 3.6.0
	 *
	 * @since 3.8.1 added \WP_Filesystem_Base.
	 *
	 * @global \WP_Filesystem_Base $wp_filesystem WordPress filesystem instance.
	 *
	 * @throws \Exception If failed to write the JSON file.
	 *
	 * @param array $data Exported data.
	 */
	private function get_json_file_data( $data ) {

		WP_Filesystem();
		global $wp_filesystem;

		$data = Helper::deep_maybe_unserialize( $data );

		// Unwrap the 'courses' array to ensure the data format is transparent and consistent across both bundled and course exports.
		if ( isset( $data['data'][0]['data'] ) && Exporter::COURSES === array_key_first( $data['data'][0]['data'] ) ) {
			$data['data'][0]['data'] = $data['data'][0]['data'][ Exporter::COURSES ];
		}

		$store = $wp_filesystem->put_contents(
			$this->get_json_file(),
			wp_json_encode( $data, JSON_PRETTY_PRINT )
		);

		if ( false === $store ) {
			throw new \Exception( esc_html__( 'Failed to write the JSON file', 'tutor-pro' ) );
		}

		return $store;
	}

	/**
	 * Get default job data
	 *
	 * @since 3.6.0
	 *
	 * @param mixed  $job_id Job id.
	 * @param string $job_requirements Job requirements.
	 *
	 * @return array
	 */
	private function get_default_job_data( $job_id, $job_requirements ) {
		$user_id = get_current_user_id();

		$data = array(
			'created_at'         => gmdate( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), tutor_time() ),
			'user_name'          => tutor_utils()->display_name( $user_id ),
			'job_id'             => $job_id ? $job_id : wp_rand(),
			'job_progress'       => '0',
			'job_status'         => 'in-progress',
			'job_requirements'   => $job_requirements,
			'message'            => __( 'Export in progress...', 'tutor-pro' ),
			'failed_message'     => '',
			'completed_contents' => array(
				tutor()->course_post_type              => array(
					'label'                 => '',
					'current_processing_id' => array(),
					'success'               => array(),
					'failed'                => array(),
				),
				tutor()->bundle_post_type              => array(
					'label'                   => '',
					'success'                 => array(),
					'failed'                  => array(),
					tutor()->course_post_type => array(
						'success' => array(),
						'failed'  => array(),
					),
				),
				$this->exporter::TYPE_CONTENT_BANK     => array(
					'label'   => '',
					'success' => array(),
					'failed'  => array(),
				),
				$this->exporter::TYPE_MEMBERSHIP_PLANS => array(
					'label'   => '',
					'success' => array(),
					'failed'  => array(),
				),
				Helper::TUTOR_COURSE_ENROLLMENTS_TYPE  => array(
					'label'   => '',
					'success' => array(),
					'failed'  => array(),
				),
				Helper::TUTOR_COURSE_REVIEW_TYPE       => array(
					'label'   => '',
					'success' => array(),
					'failed'  => array(),
				),
				Helper::TUTOR_COURSE_PROGRESS_TYPE     => array(
					'label'   => '',
					'success' => array(),
					'failed'  => array(),
				),
				Helper::TUTOR_COURSE_ORDERS_TYPE       => array(
					'label'   => '',
					'success' => array(),
					'failed'  => array(),
				),
				Helper::TUTOR_COURSE_PLANS_TYPE        => array(
					'label'   => '',
					'success' => array(),
					'failed'  => array(),
				),
				Helper::TUTOR_COURSE_SUBSCRIPTION_TYPE => array(
					'label'   => '',
					'success' => array(),
					'failed'  => array(),
				),
				'settings'                             => false,
				$this->exporter::GRADE_BOOKS_SETTINGS  => false,
			),
		);

		return apply_filters( 'tutor_pro_export_job_data', $data );
	}

	/**
	 * Fetch export/import history
	 *
	 * Retrieves the last 10 export/import operations with their status,
	 * creation date, and user information.
	 *
	 * @since 3.6.0
	 *
	 * @return void send wp_json_response
	 */
	public function ajax_fetch_history() {
		tutor_utils()->check_nonce();
		tutor_utils()->check_current_user_capability();

		$data = $this->get_export_import_history();

		$this->json_response( __( 'History fetched successfully!', 'tutor-pro' ), $data );
	}

	/**
	 * Get export/import history
	 *
	 * Unserialize the data before sending response
	 *
	 * @since 3.6.0
	 *
	 * @return array
	 */
	public function get_export_import_history(): array {
		global $wpdb;
		$data = array();

		$fetch = QueryHelper::get_all(
			$wpdb->options,
			array(
				'(option_name LIKE %s OR option_name LIKE %s) AND option_value NOT LIKE %s' => array(
					'RAW',
					array(
						'tutor_pro_export_%',
						'tutor_pro_import_%',
						'%"job_progress";i:0%',
					),
				),
			),
			'option_id',
			10
		);

		if ( ! $fetch ) {
			return $data;
		}

		foreach ( $fetch as $item ) {
			if ( ! isset( $item->option_name ) || ! isset( $item->option_value ) ) {
				continue;
			}

			$type          = strpos( $item->option_name, 'export' ) !== false ? 'export' : 'import';
			$options_value = maybe_unserialize( $item->option_value );

			if ( ! is_array( $options_value ) ) {
				continue;
			}

			$converted_item = array(
				'type'       => $type,
				'id'         => (int) ( $item->option_id ?? 0 ),
				'created_at' => ! empty( $options_value['created_at'] ) ? tutor_i18n_get_formated_date( $options_value['created_at'] ) : '',
				'user_name'  => Input::sanitize( $options_value['user_name'] ?? '' ),
				'title'      => $this->generate_progress_message( $options_value, 100, 'import' === $type )['message'],
			);

			$data[] = $converted_item;
		}

		return $data;
	}

	/**
	 * Delete export/import history
	 *
	 * @since 3.6.0
	 *
	 * @return void send wp_json_response
	 */
	public function ajax_delete_export_import_history() {
		tutor_utils()->check_nonce();
		tutor_utils()->check_current_user_capability();

		$option_id = Input::post( 'option_id', 0, Input::TYPE_INT );

		if ( ! $option_id ) {
			$this->response_bad_request( __( 'Option ID is required to delete history', 'tutor-pro' ) );
		}

		try {
			$this->delete_export_import_history( $option_id );
		} catch ( \InvalidArgumentException $e ) {
			$this->response_bad_request( $e->getMessage() );
		} catch ( \Exception $e ) {
			$this->response_bad_request( $e->getMessage() );
		}

		$this->json_response( __( 'History deleted successfully!', 'tutor-pro' ) );
	}

	/**
	 * Delete export/import history
	 *
	 * @since 3.6.0
	 *
	 * @param int $option_id Option ID to delete.
	 *
	 * @return bool|\WP_Error
	 */
	public function delete_export_import_history( $option_id = 0 ) {
		global $wpdb;

		$deleted = $wpdb->query(
			$wpdb->prepare(
				"DELETE FROM $wpdb->options
				WHERE option_id = %d
				AND (option_name LIKE %s OR option_name LIKE %s)",
				$option_id,
				'tutor_pro_export_%',
				'tutor_pro_import_%'
			)
		);

		if ( false === $deleted ) {
			return new \WP_Error( 'db_error', __( 'Database error occurred while deleting history', 'tutor-pro' ) );
		}

		return true;
	}

	/**
	 * Delete export/import history
	 *
	 * @since 3.6.0
	 *
	 * @param mixed $job_id Job ID.
	 * @param array $job_data Exported data.
	 *
	 * @return void
	 */
	public function update_settings_log( $job_id, $job_data ) {
		if ( is_array( $job_data ) && ! empty( $job_data ) ) {
			$exported_data = $job_data['data'];
			if ( ! empty( $exported_data ) ) {
				foreach ( $exported_data as $data ) {
					if ( $this->exporter::TYPE_SETTINGS === $data['content_type'] ) {
						( new Options_V2( false ) )->update_settings_log( $data['data'], 'Exported' );
					}
				}
			} else {
				global $wpdb;
				$option_name = 'tutor_pro_export_' . $job_id;

				$delete_clause = array(
					'option_name' => $option_name,
				);

				QueryHelper::delete( $wpdb->options, $delete_clause );
			}
		}
	}

	/**
	 * Get contextual sub contents
	 *
	 * If the context is content-bank then remove quiz from the list
	 * If the context is empty then remove questions from the list.
	 *
	 * @since 3.7.1
	 *
	 * @param string $context Export context.
	 * @param array  $sub_contents Sub contents.
	 *
	 * @return array
	 */
	public function get_contextual_sub_content( string $context, array $sub_contents ) {
		if ( count( $sub_contents ) ) {
			if ( $this->exporter::TYPE_CONTENT_BANK === $context ) {
				$sub_contents = array_filter(
					$sub_contents,
					fn ( $content ) => tutor()->quiz_post_type !== $content['key']
				);
			} else {
				$sub_contents = array_filter(
					$sub_contents,
					fn ( $content ) => $this->exporter::TYPE_QUESTIONS !== $content['key']
				);
			}
		}

		return array_values( $sub_contents );
	}

	/**
	 * Check if the given type is a course or a bundle.
	 *
	 * @since 3.8.1
	 *
	 * @param string $type The post type to check.
	 * @return bool True if the type is a course or bundle, false otherwise.
	 */
	private function is_course_or_bundle( string $type ): bool {

		return in_array( $type, array( tutor()->course_post_type, tutor()->bundle_post_type ) );
	}


	/**
	 * Calculate the number of sub-files that have been processed.
	 *
	 * @since 3.8.1
	 *
	 * @param int   $total_sub_files    Total number of sub-files for the content.
	 * @param array $completed_contents Data containing completed sub-files.
	 * @return int Number of sub-files that have been completed.
	 */
	private function calculate_sub_files( int $total_sub_files, array $completed_contents ): int {

		$remaining       = $completed_contents['remaining_sub_files'] ?? $total_sub_files;
		$remaining_count = is_array( $remaining ) ? count( $remaining ) : $remaining;

		return abs( $total_sub_files - $remaining_count );
	}


	/**
	 * Determine the file name for exporting the current content.
	 *
	 * @since 3.8.1
	 *
	 * @return array Array containing [parent_id, file_key].
	 */
	private function determine_export_file_name(): array {

		if ( empty( $this->content_data ) ) {
			return array( null, null );
		}

		if ( self::SETTINGS === $this->content_type ) {
			$this->content_type = '';
			return array( '', self::SETTINGS );
		}

		if ( $this->exporter::GRADE_BOOKS_SETTINGS === $this->content_type ) {
			$this->content_type = '';
			return array( '', $this->exporter::GRADE_BOOKS_SETTINGS );
		}

		if ( CollectionModel::POST_TYPE === $this->content_type && is_a( $this->content_data[0], 'WP_Post' ) ) {
			return array( '', $this->content_data[0]->ID );
		}

		$content_key = array_keys( $this->content_data );

		if ( CourseExporter::is_course( $content_key[0] ) ) {
			return array( $this->content_data['course']->ID, $this->content_data['course']->ID );
		}

		if ( CourseExporter::is_sub_file( $this->content_type, $content_key[0] ) ) {
			return array( $this->current_processing_parent_id, $content_key[0] );
		}

		if ( BundleExporter::is_bundle( $content_key[0] ) ) {
			return array( $this->content_data['bundle']->ID, $this->content_data['bundle']->ID );
		}

		if ( BundleExporter::is_sub_file( $this->content_type, $content_key[0] ) ) {
			if ( $this->exporter::COURSES === $content_key[0] ) {

				$sub_content_keys = array_keys( $this->content_data[ $content_key[0] ] );
				$file_name        = $this->exporter::COURSE === $sub_content_keys[0] ? $this->current_bundle_course_id : $sub_content_keys[0];
				$folder           = "{$this->current_processing_parent_id}/{$content_key[0]}/{$this->current_bundle_course_id}";
				return array( $folder, $file_name );
			}
			return array( $this->current_processing_parent_id, $content_key[0] );
		}

		if ( PlanExporter::is_membership_plan( $content_key[0] ) ) {
			return array( $this->current_processing_parent_id, $this->current_processing_parent_id );
		}

		if ( SubscriptionExporter::is_sub_file( $this->content_type, $content_key[0] ) ) {
			return array( $this->current_processing_parent_id, $content_key[0] );
		}

		return array( null, null );
	}

	/**
	 * Check if the current bundle course has already been exported.
	 *
	 * @since 3.8.1
	 *
	 * @param array $job_data The export job data containing job requirements.
	 *
	 * @return bool True if the course is already exported, false otherwise.
	 */
	private function is_course_already_exported( $job_data ) {

		$course_type_index = array_search( tutor()->course_post_type, array_column( $job_data['job_requirements'], 'type' ) );

		if ( false === $course_type_index ) {
			return false;
		}

		return in_array( $this->current_bundle_course_id, $job_data['job_requirements'][ $course_type_index ]['ids'] );
	}

	/**
	 * Prepare job data for export by setting flags and adding optional media/user data.
	 *
	 * @since 3.8.1
	 *
	 * @param array    $job_data Reference to the job data array.
	 * @param int|bool $keep_media_files Whether to include media files in the export.
	 * @param int|bool $keep_user_data Whether to include user data in the export.
	 * @return void
	 */
	private function prepare_job_data( &$job_data, $keep_media_files, $keep_user_data ) {

		$job_data['keep_media_files'] = $job_data['keep_media_files'] ?? $keep_media_files ?? 0;
		$job_data['keep_user_data']   = $job_data['keep_user_data'] ?? $keep_user_data ?? 0;

		if ( ! empty( $job_data['keep_media_files'] ) ) {
			$this->exporter->add_media_files();
		}

		if ( ! empty( $job_data['keep_user_data'] ) ) {
			$this->exporter->add_user_data();
			$this->exporter->set_job_data( $job_data );
		}
	}

	/**
	 * Get the remaining IDs to process by removing already completed ones.
	 *
	 * @since 3.8.1
	 *
	 * @param array $ids List of all IDs to process.
	 * @param array $completed_contents Completed contents with 'success' and 'failed' keys.
	 * @return array Remaining IDs that have not yet been processed.
	 */
	private function get_remaining_ids( array $ids, array $completed_contents ) {

		$completed_ids = array_merge( $completed_contents['success'], $completed_contents['failed'] );
		return array_diff( $ids, $completed_ids );
	}

	/**
	 * Get the current sub file being processed and the remaining sub files.
	 *
	 * @since 3.8.1
	 * @param array $completed_contents Completed contents data.
	 * @param array $sub_files List of all sub files for the export.
	 * @return array Tuple containing:
	 *               - array $remaining_course_sub_files The remaining sub files after the current one.
	 *               - mixed $current_sub_file The current sub file being processed, or null if none.
	 */
	private function get_current_and_remaining_sub_files( $completed_contents, $sub_files ) {

		$remaining_course_sub_files = ! empty( $completed_contents['remaining_sub_files'] ) ? $completed_contents['remaining_sub_files'] : $sub_files;
		$current_sub_file           = array_shift( $remaining_course_sub_files );

		return array( $remaining_course_sub_files, $current_sub_file );
	}



	/**
	 * Prepare the next course in a bundle for export, updating job data as needed.
	 *
	 * @since 3.8.1
	 *
	 * @param array $job_data Reference to the job data array.
	 * @param int   $bundle_id The ID of the bundle being exported.
	 * @param array $completed_contents Completed contents for the bundle.
	 * @param array $remaining_bundle_sub_files List of remaining sub files for the bundle.
	 * @return array Tuple containing:
	 *               - string $current_course_sub_file The current course sub file being processed.
	 *               - array $remaining_course_sub_files The remaining course sub files
	 *               - array $remaining_bundle_course_ids The remaining bundle course ids
	 */
	private function prepare_next_course_in_bundle( &$job_data, $bundle_id, $completed_contents, $remaining_bundle_sub_files ) {

		$course_post_type = tutor()->course_post_type;
		$bundle_post_type = tutor()->bundle_post_type;
		$course           = $completed_contents[ $course_post_type ];

		$bundle_course_ids           = ! empty( $course['bundle_course_ids'] ) ? $course['bundle_course_ids'] : BundleModel::get_bundle_course_ids( $bundle_id );
		$remaining_bundle_course_ids = ! empty( $course['remaining_bundle_course_ids'] ) ? $course['remaining_bundle_course_ids'] : $bundle_course_ids;

		$course_sub_files           = $this->course_exporter->get_sub_files( $job_data['keep_user_data'] );
		$remaining_course_sub_files = ! empty( $course['remaining_course_sub_files'] ) ? $course['remaining_course_sub_files'] : $course_sub_files;

		$current_course_sub_file        = current( $remaining_course_sub_files );
		$this->current_bundle_course_id = $this->exporter::COURSE === $current_course_sub_file ? current( $remaining_bundle_course_ids ) : $course['current_bundle_course_id'];

		if ( $this->is_course_already_exported( $job_data ) ) {

			array_shift( $remaining_bundle_course_ids );
			$remaining_bundle_sub_files = array( $this->exporter::COURSES );

			$job_data['completed_contents'][ $bundle_post_type ][ $course_post_type ]['remaining_bundle_course_ids'] = $remaining_bundle_course_ids;
			$job_data['completed_contents'][ $bundle_post_type ][ $course_post_type ]['remaining_course_sub_files']  = array();

			if ( empty( $remaining_bundle_course_ids ) ) {
				$job_data['completed_contents'][ $bundle_post_type ]['success'][] = $bundle_id;
				array_shift( $remaining_bundle_sub_files );
			}

			$job_data['completed_contents'][ $bundle_post_type ]['remaining_sub_files'] = $remaining_bundle_sub_files;
			$this->update_export_job( $job_data, $bundle_post_type, array() );
			$this->send_export_response( __( 'Export in progress', 'tutor-pro' ) );
		}

		return array(
			'current_course_sub_file'     => $current_course_sub_file,
			'remaining_course_sub_files'  => $remaining_course_sub_files,
			'remaining_bundle_course_ids' => $remaining_bundle_course_ids,
		);
	}

	/**
	 * Handles the download of an exported ZIP file.
	 *
	 * @since 3.8.1
	 *
	 * @return void
	 */
	public function handle_export_zip_download() {

		tutor_utils()->check_nonce();
		tutor_utils()->check_current_user_capability();

		$action    = Input::get( 'action', '', Input::TYPE_STRING );
		$file_name = Input::get( 'file', '', Input::TYPE_STRING );
		$download  = Input::get( 'download', '', Input::TYPE_BOOL );

		if ( empty( $action ) || Helper::EXPORT_ZIP_DOWNLOAD_ACTION !== $action || empty( $file_name ) ) {
			return;
		}

		$file_path = $this->upload_dir . $file_name;

		if ( false === $download ) {
			wp_delete_file( $file_path );
			return;
		}

		Helper::download_zip( $file_path );
	}
}