# wpvr/8.5.79/admin/classes/class-wpvr-ajax.php

WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress, version 8.5.79. 1,247 lines.

- Page: https://pluginprobe.com/plugins/wpvr/8.5.79/code/admin/classes/class-wpvr-ajax.php
- Raw: https://pluginprobe.com/plugins/wpvr/8.5.79/raw/admin/classes/class-wpvr-ajax.php
- Modified: 2026-08-13T12:07:48+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/wpvr/8.5.79/code/admin/classes/class-wpvr-ajax.php#L10-L20`.

```php
<?php

if (!defined('ABSPATH')) exit; // Exit if accessed directly
/**
 * The admin-specific Ajax files.
 *
 * @link       http://rextheme.com/
 * @since      8.0.0
 *
 * @package    Wpvr
 * @subpackage Wpvr/admin
 */

class Wpvr_Ajax
{

  /**
   * Instance of WPVR_Format class
   *
   * @var object
   * @since 8.0.0
   */
  protected $format;


  /**
   * Instance of WPVR_StreetView class
   *
   * @var object
   * @since 8.0.0
   */
  protected $streetview;


  /**
   * Instance of WPVR_Video class
   *
   * @var object
   * @since 8.0.0
   */
  protected $video;


  /**
   * Instance of WPVR_Scene class
   *
   * @var object
   * @since 8.0.0
   */
  protected $scene;


  /**
   * Instance of WPVR_Validator class
   *
   * @var object
   * @since 8.0.0
   */
  protected $validator;


  function __construct()
  {
    $this->format     = new WPVR_Format();
    $this->streetview = new WPVR_StreetView();
    $this->video      = new WPVR_Video();
    $this->scene      = new WPVR_Scene();
    $this->validator  = new WPVR_Validator();

    add_action('wp_ajax_wpvr_save',              array($this, 'wpvr_save_data'));
    add_action('wp_ajax_wpvr_preview',           array($this, 'wpvr_show_preview'));
    add_action('wp_ajax_wpvrstreetview_preview', array($this, 'wpvrstreetview_preview'));
    add_action('wp_ajax_wpvr_file_import',       array($this, 'wpvr_file_import'));
    add_action('wp_ajax_wpvr_role_management',   array($this, 'wpvr_role_management'));
    add_action('wp_ajax_wpvr_notice',            array($this, 'wpvr_notice'));
    add_action('wp_ajax_wpvr_dismiss_black_friday_notice',  array($this, 'dismiss_black_friday_notice'));
    add_action('wp_ajax_wpvr_review_request',  array($this, 'wpvr_review_request'));

    //setup wizard ajax
      add_action( 'wp_ajax_wpvr_create_contact', array($this, 'wpvr_create_contact' ) );

      //general setting ajax
      add_action( 'wp_ajax_wpvr_save_general_settings', array($this, 'wpvr_save_general_settings' ) );
      // opt-in toggle ajax
      add_action( 'wp_ajax_wpvr_save_opt_in_toggle', array($this, 'wpvr_save_opt_in_toggle' ) );

      // Setup wizard specific AJAX handlers
      add_action( 'wp_ajax_wpvr_fetch_template', array($this, 'wpvr_fetch_template' ) );
      add_action( 'wp_ajax_wpvr_upload_image', array($this, 'wpvr_upload_image' ) );
      add_action( 'wp_ajax_wpvr_create_tour_from_wizard', array($this, 'wpvr_create_tour_from_wizard' ) );
  }


  public function wpvr_review_request()
  {
      if( !current_user_can( 'manage_options' ) ){
          wp_send_json_error( array( 'message' => 'Unauthorized user' ), 403 );
          return;
      }
    $nonce  = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
    if ( ! wp_verify_nonce( $nonce, 'wpvr-dismiss-notice-five-star-review' ) ) {
      $response = array(
        'success' => false,
        'data' => 'Permission denied.'
      );
      wp_send_json($response);
    }
    $payload = !empty($_POST['payload']) ? map_deep( wp_unslash( $_POST['payload'] ), 'sanitize_text_field' ) : array();
    $data = array(
      'show'      => !empty($payload['show']) ? $payload['show'] : '',
      'time'      => !empty($payload['frequency']) && 'never' !== $payload['frequency'] ? time() : '',
      'frequency' => !empty($payload['frequency']) ? $payload['frequency'] : '',
    );
    update_option('wpvr_feed_review_request', $data);
    $response = array(
      'success' => true,
      'data' => 'Review request updated successfully.'
    );
    wp_send_json($response);
    die();
  }

  /**
   * Responsible for Tour Preview
   *
   * @return void
   * @since 8.0.0
   */
  public function wpvr_show_preview()
  {
    //===Current user capabilities check===//
    if (!current_user_can('edit_posts')) {
      $response = array(
        'success'   => false,
        'data'  => 'Contact admin.'
      );
      wp_send_json($response);
    }
    //===Current user capabilities check===//
    //===Nonce check===//
    $nonce  = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
    if ( ! wp_verify_nonce( $nonce, 'wpvr' ) ) {
      $response = array(
        'success'   => false,
        'data'  => 'Permission denied.'
      );
      wp_send_json($response);
    }
    //===Nonce check===//

    $panoid = '';
    $postid = isset( $_POST['postid'] ) ? sanitize_text_field( wp_unslash( $_POST['postid'] ) ) : 0;
    $panoid = 'pano' . $postid;
    $panovideo = 'off';
    if ( isset( $_POST['panovideo'] ) ) {
      $panovideo = sanitize_text_field( wp_unslash( $_POST['panovideo'] ) );
    }

    $post_type = get_post_type($postid);
    if ($post_type != 'wpvr_item') {
      die();
    }

    do_action('wpvr_pro_street_view_preview', $postid, $panoid);

    if ($panovideo == 'off') {
      $this->scene->wpvr_scene_preview($panoid, $panovideo);     // Preapre preview based on Scene data //
    } else {
      $this->video->wpvr_video_preview($panoid);                 // Prepare preview based on Video data //
    }
  }


  /**
   * Responsible for saving WPVR data
   *
   * @return void
   * @since 8.0.0
   */
  public function wpvr_save_data()
  {
    /**
	 * Verify current user has permission to perform this action.
	 *
	 * @return void
	 */
    if ( ! current_user_can('edit_posts') ) {
		wp_send_json([
			'success' => false,
			'data'    => 'Permission denied.'
		]);
	}

    /**
	 * Validate AJAX nonce to prevent unauthorized or forged requests.
	 *
	 * @return void
	 */
    $nonce  = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
	if ( ! wp_verify_nonce( $nonce, 'wpvr' ) ) {
		wp_send_json([
			'success' => false,
			'data'    => 'Invalid or expired request.',
		]);
	}


    $postid = isset( $_POST['postid'] ) ? absint( sanitize_text_field( wp_unslash( $_POST['postid'] ) ) ) : 0;

	/**
	 * Ensures a valid post ID is supplied before proceeding.
	 *
	 * @return void
	 */
    if($postid < 1) {
		wp_send_json_error([
            'success' => false,
            'data' => '<span class="pano-error-title">Invalid post ID</span> <p>Malformed data passed.</p>'
		]);
        die();
    }

    /**
	 * Ensures the post type is 'wpvr_item' before proceeding.
	 *
	 * @return void
	 */
    $post_type = get_post_type( $postid );
    if ($post_type != 'wpvr_item') {
      die();
    }

    $panoid = 'pano' . $postid;
    

    /**
	 * Checks if this is a publish action and validates scene/video data.
	 *
	 * @return void
	 */
    $action_type = isset($_POST['action_type']) ? sanitize_text_field(wp_unslash( $_POST['action_type'] )) : 'auto-draft';
    $is_publish_action = ($action_type === 'publish');

    /**
     * Checks if title is provided FIRST before any other validation.
     *
     * @return void
     */
    if ($is_publish_action && (!isset($_POST['post_title']) || empty(trim(sanitize_text_field(wp_unslash( $_POST['post_title'] )))))) {
      wp_send_json([
        'success' => false,
        'data' => '<span class="pano-error-title">Title Required!</span> <p>Please provide a title for this tour before publishing.</p>'
      ]);
      die();
    }

    /**
	 * Validates scene/video data before allowing publication.
	 *
	 * @return void
	 */
    $is_street_view_mode = (!empty($_POST['streetview']) && sanitize_text_field(wp_unslash( $_POST['streetview'] )) == 'on');

    if ($is_publish_action) {

      $has_scene_data = false;
      $has_video_data = false;
      $is_video_mode = false;
      $has_street_view_data = false;

      // Check if video mode is enabled
      if (isset($_POST['panovideo']) && sanitize_text_field(wp_unslash( $_POST['panovideo'] )) === 'on') {
        $is_video_mode = true;
        if (isset($_POST['videourl']) && !empty($_POST['videourl'])) {
          $has_video_data = true;
        }
      } elseif (!empty($_POST['streetview']) && sanitize_text_field(wp_unslash( $_POST['streetview'] )) == 'on') {
        // Check if Street View mode is enabled (Pro feature)
        $is_street_view_mode = true;
          if (!empty($_POST['streetviewurl'])) {
              $has_street_view_data = true;
          }
        // Street View doesn't require scene data as it uses Google Street View API
      } else {
        // Check for scene data
        if (isset($_POST['panodata']) && !empty($_POST['panodata'])) {
          $panodata = json_decode( wp_unslash( $_POST['panodata'] ), true ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
          if (isset($panodata['scene-list']) && !empty($panodata['scene-list'])) {
            foreach ($panodata['scene-list'] as $scene) {
              // Check if it's a cubemap scene
              if (isset($scene['scene-type']) && $scene['scene-type'] === 'cubemap') {
                // Check all six faces of the cube
                $required_faces = array(
                  'scene-attachment-url-face0',
                  'scene-attachment-url-face1',
                  'scene-attachment-url-face2',
                  'scene-attachment-url-face3',
                  'scene-attachment-url-face4',
                  'scene-attachment-url-face5'
                );

                $missing_faces = array();
                foreach ($required_faces as $face) {
                  if (empty($scene[$face])) {
                    $missing_faces[] = $face;
                  }
                }

                if (!empty($missing_faces)) {
                  $response = array(
                    'success' => false,
                    'data' => '<span class="pano-error-title">Incomplete Cubemap Scene!</span> <p>Please add images for all six faces of the cube. Missing faces: ' . implode(', ', array_map(function($face) { return str_replace('scene-attachment-url-', '', $face); }, $missing_faces)) . '</p>'
                  );
                  wp_send_json($response);
                  die();
                }

                if (!empty($scene['scene-id'])) {
                  $has_scene_data = true;
                }
              } else {
                // Regular equirectangular scene check
                if (!empty($scene['scene-id']) && !empty($scene['scene-attachment-url'])) {
                  $has_scene_data = true;
                  break;
                }
              }
            }
          }
        }
      }

      // Provide specific error messages based on the mode and missing data
      if ($is_video_mode && !$has_video_data) {
        // Video mode is enabled but no video URL provided
        $response = array(
          'success' => false,
          'data' => '<span class="pano-error-title">No Video Data Found!</span> <p>Please add a video URL in the video settings before publishing this tour.</p>'
        );
        wp_send_json($response);
        die();
      } elseif($is_street_view_mode && !$has_street_view_data) {
          $response = array(
              'success' => false,
              'data' => '<span class="pano-error-title">No Street View Data Found!</span> <p>Please add a street view URL in the street view settings before publishing this tour.</p>'
          );
          wp_send_json($response);
          die();
      }elseif (!$is_video_mode && !$is_street_view_mode && !$has_scene_data) {
        // Scene mode but no valid scenes found (exclude Street View from this check)
        $response = array(
          'success' => false,
          'data' => '<span class="pano-error-title">No Scene Data Found!</span> <p>Please add at least one scene with an image before publishing this tour.</p>'
        );
        wp_send_json($response);
        die();
      }
    }

    $post_array = array(
		'post_status'   => get_post_status( $postid ),
		'post_password' => get_post_field( 'post_password', $postid ),
		'visibility'    => 'public',
	);

	if ( isset( $_POST['post_status'] ) ) {
		$post_status               = sanitize_text_field( wp_unslash( $_POST['post_status'] ) );
		$post_array['post_status'] = $post_status;
	}
	if ( isset( $_POST['post_password'] ) ) {
		$post_password               = sanitize_text_field( wp_unslash( $_POST['post_password'] ) );
		$post_array['post_password'] = $post_password;
	}
	if ( isset( $_POST['visibility'] ) ) {
		$visibility               = sanitize_text_field( wp_unslash( $_POST['visibility'] ) );
		$post_array['visibility'] = $visibility;
		if ( $visibility == 'public' || $visibility == 'private' ) {
			$post_array['post_password'] = '';
		}
	}

	if ( $post_array['visibility'] == 'private' ) {
		$post_array['post_status'] = 'private';
	} elseif ( $is_publish_action ) {
		$post_array['post_status'] = 'publish';
	} else {
		// Keep current status or set to draft if it's auto-draft
		$current_status = get_post_status( $postid );
		if ( $current_status === 'auto-draft' ) {
			$post_array['post_status'] = 'draft';
		}
	}

	$post_title = isset( $_POST['post_title'] ) ? sanitize_text_field( wp_unslash( $_POST['post_title'] ) ) : get_the_title( $postid );
	wp_update_post( array(
		'ID'            => $postid,
		'post_status'   => $post_array['post_status'],
		'post_password' => $post_array['post_password'],
		'post_title'    => $post_title,
	) );

	do_action( 'wpvr_pro_update_street_view', $postid, $panoid );

	if ( isset( $_POST['checklistData'] ) && !empty( $_POST['checklistData'] ) ) {
		$checklist_data = array_map( 'sanitize_text_field', wp_unslash( $_POST['checklistData'] ) );
		update_post_meta( $postid, 'wpvr_checklist', $checklist_data );
	}
	if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
        error_log("Tour with ID $postid has been saved with status " . $post_array['post_status']);
    }

	if ( ! $is_street_view_mode ) {
		if ( isset( $_POST['panovideo'] ) && sanitize_text_field( wp_unslash( $_POST['panovideo'] ) ) == 'on' ) {
			$this->video->wpvr_update_meta_box( $postid, $panoid, $is_publish_action );
		} else {
			$this->scene->wpvr_update_meta_box( $postid, $panoid, $is_publish_action );
		}
	}

    do_action('wpvr_rex_wpvr_tour_saved', $postid);

    $response = array(
      'success'   => true,
      'data'  => array(
        'post_ID' => $postid,
        'post_status' => get_post_status($postid)
      )
    );
    wp_send_json($response);
    die();
  }


  /**
   * Responsible for importing tour
   *
   * @return void
   * @since 8.0.0
   */
  public function wpvr_file_import()
  {
    //===Current user capabilities check===//
    if (!current_user_can('edit_posts')) {
      $response = array(
        'success'   => false,
        'data'  => 'Permission denied.'
      );
      wp_send_json($response);
    }
    //===Current user capabilities check===//
    //===Nonce check===//
    $nonce  = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
    if ( ! wp_verify_nonce( $nonce, 'wpvr' ) ) {
      $response = array(
        'success'   => false,
        'data'  => 'Permission denied.'
      );
      wp_send_json($response);
    }
    $file_name = '';

      if ( isset( $_FILES['wpvr_import_file'] ) && ! empty( $_FILES['wpvr_import_file']['tmp_name'] ) ) {
          $file = $_FILES['wpvr_import_file'];

          // Validate file type - check if it's a ZIP file
          $file_type = wp_check_filetype($file['name']);
          $file_ext = strtolower($file_type['ext']);
          if ($file_ext !== 'zip') {
              wp_send_json_error(array('message' => 'Invalid file format. Only ZIP files are allowed.'));
              return;
          }

          // Define a temporary filter to change the upload directory to our temp folder
          $upload_dir_filter = function( $upload ) {
              $upload['subdir'] = '/wpvr_imported_temp';
              $upload['path']   = $upload['basedir'] . $upload['subdir'];
              $upload['url']    = $upload['baseurl'] . $upload['subdir'];
              return $upload;
          };

          // Apply the filter
          add_filter( 'upload_dir', $upload_dir_filter );

          // Use wp_handle_upload to securely handle the uploaded file
          $upload_overrides = array( 'test_form' => false );
          $movefile = wp_handle_upload( $file, $upload_overrides );

          // Remove the filter immediately after upload
          remove_filter( 'upload_dir', $upload_dir_filter );

          if ( $movefile && ! isset( $movefile['error'] ) ) {
              // Use the sanitized file name generated by WordPress
              $file_name = basename( $movefile['file'] );
          } else {
              wp_send_json_error( array( 'message' => $movefile['error'] ) );
              return;
          }

      } else {
          wp_send_json_error( array( 'message' => 'No file selected.' ) );
          return;
      }

    //===Nonce check===//
    WPVR_Import::prepare_tour_import_feature($file_name);
  }



  /**
   * WPVR Role Management
   *
   * @return void
   * @since 8.0.0
   */
  function wpvr_role_management()
  {

    //===Current user capabilities check===//
    if (!current_user_can('manage_options')) {
      $response = array(
        'success'   => false,
        'data'  => 'Permission denied.'
      );
      wp_send_json($response);
    }
    //===Current user capabilities check===//
    //===Nonce check===//
    $nonce  = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
    if ( ! wp_verify_nonce( $nonce, 'wpvr' ) ) {
      $response = array(
        'success'   => false,
        'data'  => 'Permission denied.'
      );
      wp_send_json($response);
    }
    //===Nonce check===//

    $editor = isset( $_POST['editor'] ) ? sanitize_text_field( wp_unslash( $_POST['editor'] ) ) : '';
    $author = isset( $_POST['author'] ) ? sanitize_text_field( wp_unslash( $_POST['author'] ) ) : '';
    $fontawesome = isset( $_POST['fontawesome'] ) ? sanitize_text_field( wp_unslash( $_POST['fontawesome'] ) ) : '';

    $cardboard = !empty($_POST['wpvr_cardboard_disable']) ? sanitize_text_field(wp_unslash( $_POST['wpvr_cardboard_disable'] )) : 'no'; //

    $wpvr_webp_conversion = !empty($_POST['wpvr_webp_conversion']) ? sanitize_text_field(wp_unslash( $_POST['wpvr_webp_conversion'] )) : 'no';

    $mobile_media_resize = isset( $_POST['mobile_media_resize'] ) ? sanitize_text_field( wp_unslash( $_POST['mobile_media_resize'] ) ) : '';
    $high_res_image = isset( $_POST['high_res_image'] ) ? sanitize_text_field( wp_unslash( $_POST['high_res_image'] ) ) : '';
    $dis_on_hover = isset( $_POST['dis_on_hover'] ) ? sanitize_text_field( wp_unslash( $_POST['dis_on_hover'] ) ) : '';
    $wpvr_mobile_hotspot_tip = isset( $_POST['wpvr_mobile_hotspot_tip'] ) ? sanitize_text_field( wp_unslash( $_POST['wpvr_mobile_hotspot_tip'] ) ) : '';
    $wpvr_frontend_notice = isset( $_POST['wpvr_frontend_notice'] ) ? sanitize_text_field( wp_unslash( $_POST['wpvr_frontend_notice'] ) ) : '';
    $wpvr_frontend_notice_area = isset( $_POST['wpvr_frontend_notice_area'] ) ? sanitize_text_field( wp_unslash( $_POST['wpvr_frontend_notice_area'] ) ) : '';
    $wpvr_script_control = isset( $_POST['wpvr_script_control'] ) ? sanitize_text_field( wp_unslash( $_POST['wpvr_script_control'] ) ) : '';
    $wpvr_script_list = isset( $_POST['wpvr_script_list'] ) ? sanitize_text_field( wp_unslash( $_POST['wpvr_script_list'] ) ) : '';

    $wpvr_video_script_control = isset( $_POST['wpvr_video_script_control'] ) ? sanitize_text_field( wp_unslash( $_POST['wpvr_video_script_control'] ) ) : '';
    $wpvr_video_script_list = isset( $_POST['wpvr_video_script_list'] ) ? sanitize_text_field( wp_unslash( $_POST['wpvr_video_script_list'] ) ) : '';

    //        $enable_woocommerce = sanitize_text_field(wp_unslash( $_POST['woocommerce'] ));

    $wpvr_script_list = str_replace(' ', '', $wpvr_script_list);

    update_option('wpvr_editor_active', $editor);
    update_option('wpvr_author_active', $author);
    update_option('wpvr_fontawesome_disable', $fontawesome);
    update_option('wpvr_cardboard_disable', $cardboard);
    update_option('wpvr_webp_conversion', $wpvr_webp_conversion);
    update_option('mobile_media_resize', $mobile_media_resize);
    update_option('high_res_image', $high_res_image);
    update_option('dis_on_hover', $dis_on_hover);
    update_option('wpvr_mobile_hotspot_tip', $wpvr_mobile_hotspot_tip ? 'true' : 'false');
    update_option('wpvr_frontend_notice', $wpvr_frontend_notice);
    update_option('wpvr_frontend_notice_area', $wpvr_frontend_notice_area);
    update_option('wpvr_script_control', $wpvr_script_control);
    update_option('wpvr_script_list', $wpvr_script_list);

    update_option('wpvr_video_script_control', $wpvr_video_script_control);
    update_option('wpvr_video_script_list', $wpvr_video_script_list);

    if(is_plugin_active( 'dokan-lite/dokan.php' ) || is_plugin_active( 'dokan-pro/dokan.php' )){
        $dokan_vendor = isset( $_POST['dokan_vendor'] ) ? sanitize_text_field(wp_unslash( $_POST['dokan_vendor'] )) : false;
        update_option('dokan_vendor_active', $dokan_vendor);
    }

    // Usage data sharing toggle — sync with Linno telemetry SDK.
    if ( isset( $_POST['wpvr_usage_tracking'] ) ) {
        $tracking_toggle  = sanitize_text_field( wp_unslash( $_POST['wpvr_usage_tracking'] ) );
        $consent_state    = 'true' === $tracking_toggle ? 'yes' : 'no';
        $opt_in_numeric   = 'yes' === $consent_state ? '1' : '0';

        update_option( 'wpvr_allow_tracking', $consent_state );
        update_option( 'wpvr_opt_in_toggle', $opt_in_numeric );

        if ( function_exists( 'linno_telemetry' ) && defined( 'WPVR_FILE' ) ) {
            $telemetry_client = linno_telemetry( WPVR_FILE );
            if ( $telemetry_client && method_exists( $telemetry_client, 'set_optin_state' ) ) {
                $telemetry_client->set_optin_state( $consent_state );
            } elseif ( function_exists( 'linno_telemetry_sync_consent_state' ) ) {
                linno_telemetry_sync_consent_state( WPVR_FILE );
            }
        }

        if ( 'yes' === $consent_state ) {
            do_action( 'wpvr_telemetry_consent_granted' );
        }
    }

    //        update_option('wpvr_enable_woocommerce', $enable_woocommerce);

    $response = array(
      'status' => 'success',
      'message' => 'Successfully saved',
    );
    wp_send_json($response);
  }


  /**
   * WPVR Notice
   *
   * @return void
   * @since 8.0.0
   */
  function wpvr_notice()
  {
    //===Current user capabilities check===//
    if (!current_user_can('manage_options')) {
      $response = array(
        'success'   => false,
        'data'  => 'Permission denied.'
      );
      wp_send_json($response);
    }
    //===Current user capabilities check===//
    //===Nonce check===//
    $nonce  = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
    if ( ! wp_verify_nonce( $nonce, 'wpvr' ) ) {
      $response = array(
        'success'   => false,
        'data'  => 'Permission denied.'
      );
      wp_send_json($response);
    }
    //===Nonce check===//
    update_option('wpvr_black_friday_notice', '1');
  }

  /**
   * Dismiss black friday notice
   */
  function dismiss_black_friday_notice(){
      if( !current_user_can( 'manage_options' ) ){
          wp_send_json_error( array( 'message' => 'Unauthorized user' ), 403 );
          return;
      }
    if (!isset($_POST['nonce']) || !wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'wpvr')) {
      wp_die(esc_html__('Permission check failed', 'wpvr'));
    }
    update_option('_wpvr_eid_al_adha_2024', 'yes');
    echo json_encode(['success' => true,]);
    wp_die();
  }

/**
 * Handles the creation of a contact via a webhook.
 *
 * This function validates the nonce, sanitizes and validates the input fields,
 * and then creates a new contact using the WPVR_Create_Contact class.
 *
 * @since 8.4.10
 */
  function wpvr_create_contact(){
      if( !current_user_can( 'manage_options' ) ){
          wp_send_json_error( array( 'message' => 'Unauthorized user' ), 403 );
          return;
      }
      $nonce = filter_input(INPUT_POST, 'security', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
      $nonce = !empty( $nonce ) ? $nonce : null;
      if ( !wp_verify_nonce( $nonce, 'wpvr' ) ) {
            wp_send_json_error( array( 'message' => 'Invalid nonce' ), 400 );
            return;
        }

      $name = filter_input(INPUT_POST, 'name', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
      $industry = filter_input(INPUT_POST, 'industry', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
      $email = filter_input(INPUT_POST, 'email', FILTER_SANITIZE_EMAIL);
      $opt_in = filter_input(INPUT_POST, 'opt_in', FILTER_SANITIZE_FULL_SPECIAL_CHARS);

      $name = !empty($name) ? $name: '';
      $industry = !empty($industry ) ? $industry : '';
      $email = !empty( $email ) ? $email : '';

        if ( empty( $email ) ) {
          wp_send_json_error( array( 'message' => __('Email is required', 'wpvr') ), 400 );
        }elseif( !is_email( $email ) ){
          wp_send_json_error( array( 'message' => __('Email is invalid', 'wpvr') ), 400 );
        }

        $create_contact_instance = new WPVR_Create_Contact( $email, $name, $industry );
        $response = $create_contact_instance->create_contact_via_webhook();

        update_option('wpvr_posthog_access_enabled', $opt_in);


        if ( $response ) {
            wp_send_json_success( array( 'message' => __('Contact created successfully', 'wpvr') ), 200 );
        } else {
            wp_send_json_error( array( 'message' => __('Failed to create contact', 'wpvr') ), 500 );
        }
    }

    /**
     * Saves the general settings for the WPVR plugin.
     *
     * This function handles the nonce verification, sanitizes the input fields,
     * and updates the options in the database. It responds with a JSON success or error message.
     *
     * @since 8.4.10
     */
  function wpvr_save_general_settings(){

      if ( ! current_user_can( 'manage_options' ) ) {
          wp_send_json_error( array( 'message' => 'Unauthorized user' ), 403 );
          return;
      }

      $nonce = filter_input(INPUT_POST, 'security', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
      $nonce = !empty( $nonce ) ? $nonce : null; // phpcs:ignore
      if ( !wp_verify_nonce( $nonce, 'wpvr' ) ) {
          wp_send_json_error( array( 'message' => 'Invalid nonce' ), 400 );
          return;
      }

      $is_mobile_media_resize = filter_input(INPUT_POST, 'media_resizer', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
      $convert_to_webp = filter_input(INPUT_POST, 'convert_to_webp', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
      $vr_glass_support = filter_input(INPUT_POST, 'vr_glass_support', FILTER_SANITIZE_FULL_SPECIAL_CHARS);

      update_option('mobile_media_resize', $is_mobile_media_resize);
      update_option('wpvr_webp_conversion', $convert_to_webp);
      update_option('wpvr_cardboard_disable', $vr_glass_support);

      wp_send_json_success( array( 'message' => __('General setting data successfully saved.', 'wpvr') ), 200 );
  }


  /**
   * AJAX handler to persist opt-in toggle value
   * 
   */
  public function wpvr_save_opt_in_toggle() {
    if ( ! current_user_can( 'manage_options' ) ) {
      wp_send_json_error( array( 'message' => 'Unauthorized user' ), 403 );
      return;
    }

    $nonce = isset($_POST['security']) ? sanitize_text_field(wp_unslash( $_POST['security'] )) : '';
    if ( !wp_verify_nonce( $nonce, 'wpvr' ) ) {
      wp_send_json_error( array( 'message' => 'Invalid nonce' ), 400 );
      return;
    }

    $opt_in = isset($_POST['opt_in']) ? sanitize_text_field(wp_unslash( $_POST['opt_in'] )) : '0';
    $consent_state = '1' === $opt_in ? 'yes' : 'no';

    update_option('wpvr_opt_in_toggle', $opt_in);
    update_option('wpvr_allow_tracking', $consent_state);

    if ( 'yes' === $consent_state ) {
      $this->wpvr_create_contact_for_current_user();
    }

    if ( function_exists( 'linno_telemetry' ) && defined( 'WPVR_FILE' ) ) {
      $telemetry_client = linno_telemetry( WPVR_FILE );

      if ( $telemetry_client && method_exists( $telemetry_client, 'set_optin_state' ) ) {
        $telemetry_client->set_optin_state( $consent_state );
      } elseif ( function_exists( 'linno_telemetry_sync_consent_state' ) ) {
        linno_telemetry_sync_consent_state( WPVR_FILE );
      }
    }

    // Fire after SDK consent is fully synced so consent-gated events can queue.
    if ( 'yes' === $consent_state ) {
      do_action( 'wpvr_telemetry_consent_granted' );
    }

    wp_send_json_success( array( 'message' => __('Opt-in value saved.', 'wpvr') ), 200 );
  }


  /**
   * Create webhook contact from current user after consent.
   *
   * @return void
   */
  private function wpvr_create_contact_for_current_user() {
    $current_user = wp_get_current_user();
    if ( ! $current_user || empty( $current_user->user_email ) ) {
      return;
    }

    $email = sanitize_email( $current_user->user_email );
    if ( ! is_email( $email ) ) {
      return;
    }

    $name = sanitize_text_field( $current_user->display_name );
    if ( empty( $name ) ) {
      $name = sanitize_text_field( $current_user->user_login );
    }

    $industry = sanitize_text_field( get_option( 'wpvr_industry_name', '' ) );

    $create_contact_instance = new WPVR_Create_Contact( $email, $name, $industry );
    $create_contact_instance->create_contact_via_webhook();
  }


  /**
   * Fetch template tour object from remote API
   * 
   * @since 8.5.48
   */
  public function wpvr_fetch_template() {
    if ( ! current_user_can( 'manage_options' ) ) {
      wp_send_json_error( array( 'message' => 'Unauthorized user' ), 403 );
      return;
    }

    $nonce = isset($_POST['security']) ? sanitize_text_field(wp_unslash( $_POST['security'] )) : '';
    if ( !wp_verify_nonce( $nonce, 'wpvr' ) ) {
      wp_send_json_error( array( 'message' => 'Invalid nonce' ), 400 );
      return;
    }

    $industry = isset($_POST['industry']) ? sanitize_text_field(wp_unslash( $_POST['industry'] )) : 'real-estate';

    // Static industry to remote tour ID mapping
    $industry_id_map = array(
      'exhibitions'  => 2140,
      'offices'      => 2145,
      'real-estate'  => 2147,
      'hotel'        => 2149,
      'ecommerce'    => 2151,
      'showrooms'    => 2153,
      'school'       => 2155,
    );

    // Get source tour ID for the selected industry
    $source_tour_id = isset($industry_id_map[$industry]) ? $industry_id_map[$industry] : 2147;

    // Build API URL with source tour ID
    $api_url = 'https://showcase.rextheme.com/wp-json/wpvr/v1/tour/' . intval($source_tour_id);
    $api_url = apply_filters('wpvr_template_api_url', $api_url, $industry, $source_tour_id);
    $response = wp_remote_get($api_url, array(
      'timeout' => 30,
      'headers' => array(
        'Content-Type' => 'application/json',
        'Accept' => 'application/json',
      ),
    ));

    if ( is_wp_error( $response ) ) {
      wp_send_json_error( array( 'message' => 'Failed to fetch template: ' . $response->get_error_message() ) );
      return;
    }

    $status_code = wp_remote_retrieve_response_code( $response );
    if ( $status_code !== 200 ) {
      wp_send_json_error( array( 'message' => 'Template not found (HTTP ' . $status_code . ')' ) );
      return;
    }

    $body = wp_remote_retrieve_body( $response );
    $api_data = json_decode( $body, true );

    if ( ! $api_data || ! is_array( $api_data ) ) {
      wp_send_json_error( array( 'message' => 'Invalid template data received' ) );
      return;
    }

    $remote_meta = array();
    if ( isset( $api_data['meta_data'] ) && is_array( $api_data['meta_data'] ) ) {
      $remote_meta = $api_data['meta_data'];
    } elseif ( isset( $api_data['meta'] ) && is_array( $api_data['meta'] ) ) {
      $remote_meta = $api_data['meta'];
    }

    $panodata = array();
    if ( isset( $remote_meta['panodata'] ) ) {
      $panodata = $this->wpvr_normalize_panodata( $remote_meta['panodata'] );
    }
    if ( empty( $panodata ) && isset( $api_data['panodata'] ) ) {
      $panodata = $this->wpvr_normalize_panodata( $api_data['panodata'] );
    }

    if ( empty( $panodata ) ) {
      wp_send_json_error( array( 'message' => 'Template panodata missing in API response' ) );
      return;
    }

    $title = isset( $api_data['title'] ) && ! empty( $api_data['title'] )
      ? sanitize_text_field( $api_data['title'] )
      : 'My Virtual Tour';

    $post_data = array(
      'post_title'   => $title,
      'post_status'  => 'publish',
      'post_type'    => 'wpvr_item',
      'post_author'  => get_current_user_id(),
    );

    $post_id = wp_insert_post( $post_data );
    if ( is_wp_error( $post_id ) ) {
      wp_send_json_error( array( 'message' => 'Failed to create tour: ' . $post_id->get_error_message() ) );
      return;
    }

    $panodata = $this->wpvr_import_scene_attachments_to_media( $panodata, $post_id );
    $panodata['panoid'] = 'pano' . $post_id;

    // Keep meta panodata in sync with imported local scene URLs
    if ( ! is_array( $remote_meta ) ) {
      $remote_meta = array();
    }
    $remote_meta['panodata'] = $panodata;

    update_post_meta( $post_id, 'panodata', $panodata );
    update_post_meta( $post_id, 'wpvr_created_from_wizard', true );
    update_post_meta( $post_id, 'wpvr_wizard_industry', $industry );

    if ( ! empty( $remote_meta ) ) {
      foreach ( $remote_meta as $meta_key => $meta_value ) {
        $sanitized_key = sanitize_key( $meta_key );
        if ( empty( $sanitized_key ) || 'panodata' === $sanitized_key ) {
          continue;
        }

        if ( is_array( $meta_value ) ) {
          update_post_meta( $post_id, $sanitized_key, $meta_value );
        } else {
          update_post_meta( $post_id, $sanitized_key, sanitize_text_field( $meta_value ) );
        }
      }
    }

    $template_data = array(
      'industry' => $industry,
      'template_id' => $source_tour_id,
      'post_id' => $post_id,
      'edit_url' => admin_url( 'post.php?action=edit&post=' . $post_id ),
      'view_url' => get_permalink( $post_id ),
      'panodata' => $panodata,
      'meta' => $remote_meta,
    );

    if ( isset( $panodata['panodata']['scene-list']['1']['scene-attachment-url'] ) ) {
      $template_data['image_url'] = esc_url_raw( $panodata['panodata']['scene-list']['1']['scene-attachment-url'] );
    } elseif ( isset( $api_data['image_url'] ) ) {
      $template_data['image_url'] = esc_url_raw( $api_data['image_url'] );
    } elseif ( isset( $api_data['featured_image'] ) ) {
      $template_data['image_url'] = esc_url_raw( $api_data['featured_image'] );
    }

    do_action('wpvr_rex_wpvr_tour_saved', $post_id);

    wp_send_json_success( array( 'template' => $template_data ) );
  }

  /**
   * Import scene attachment URLs into media library and replace URLs in panodata.
   *
   * @param array $panodata Panodata structure.
   * @param int   $post_id  Target post ID.
   *
   * @return array
   */
  private function wpvr_import_scene_attachments_to_media( $panodata, $post_id ) {
    if ( empty( $panodata['panodata']['scene-list'] ) || ! is_array( $panodata['panodata']['scene-list'] ) ) {
      return $panodata;
    }

    require_once( ABSPATH . 'wp-admin/includes/file.php' );
    require_once( ABSPATH . 'wp-admin/includes/media.php' );
    require_once( ABSPATH . 'wp-admin/includes/image.php' );

    $scene_image_keys = array(
      'scene-attachment-url',
      'scene-attachment-url-face0',
      'scene-attachment-url-face1',
      'scene-attachment-url-face2',
      'scene-attachment-url-face3',
      'scene-attachment-url-face4',
      'scene-attachment-url-face5',
    );

    $imported_urls = array();

    foreach ( $panodata['panodata']['scene-list'] as $scene_key => $scene ) {
      if ( ! is_array( $scene ) ) {
        continue;
      }

      foreach ( $scene_image_keys as $image_key ) {
        if ( empty( $scene[ $image_key ] ) || ! is_string( $scene[ $image_key ] ) ) {
          continue;
        }

        $source_url = esc_url_raw( $scene[ $image_key ] );
        if ( empty( $source_url ) ) {
          continue;
        }

        if ( isset( $imported_urls[ $source_url ] ) ) {
          $panodata['panodata']['scene-list'][ $scene_key ][ $image_key ] = $imported_urls[ $source_url ];
          continue;
        }

        $attachment_id = attachment_url_to_postid( $source_url );
        if ( ! $attachment_id ) {
          $attachment_id = media_sideload_image( $source_url, $post_id, null, 'id' );
          if ( is_wp_error( $attachment_id ) ) {
            continue;
          }
        }

        $local_url = wp_get_attachment_url( $attachment_id );
        if ( ! empty( $local_url ) ) {
          $local_url = esc_url_raw( $local_url );
          $imported_urls[ $source_url ] = $local_url;
          $panodata['panodata']['scene-list'][ $scene_key ][ $image_key ] = $local_url;
        }
      }
    }

    return $panodata;
  }

  /**
   * Normalize panodata payloads from array/serialized/json values.
   *
   * @param mixed $raw_panodata Panodata from remote API/meta.
   *
   * @return array
   */
  private function wpvr_normalize_panodata( $raw_panodata ) {
    if ( is_array( $raw_panodata ) ) {
      return $raw_panodata;
    }

    if ( is_string( $raw_panodata ) && '' !== $raw_panodata ) {
      $unserialized = maybe_unserialize( $raw_panodata );
      if ( is_array( $unserialized ) ) {
        return $unserialized;
      }

      $decoded_json = json_decode( $raw_panodata, true );
      if ( is_array( $decoded_json ) ) {
        return $decoded_json;
      }
    }

    return array();
  }

  /**
   * Upload image to WordPress media library
   * 
   * @since 8.5.48
   */
  public function wpvr_upload_image() {
    if ( ! current_user_can( 'upload_files' ) ) {
      wp_send_json_error( array( 'message' => 'Unauthorized user' ), 403 );
      return;
    }

    $nonce = isset($_POST['security']) ? sanitize_text_field(wp_unslash( $_POST['security'] )) : '';
    if ( !wp_verify_nonce( $nonce, 'wpvr' ) ) {
      wp_send_json_error( array( 'message' => 'Invalid nonce' ), 400 );
      return;
    }

    if ( ! isset( $_FILES['image'] ) || empty( $_FILES['image']['tmp_name'] ) ) {
      wp_send_json_error( array( 'message' => 'No file uploaded' ) );
      return;
    }

    $file_name = isset( $_FILES['image']['name'] ) ? sanitize_file_name( wp_unslash( $_FILES['image']['name'] ) ) : '';
    $file_type = wp_check_filetype( $file_name );
    $allowed_types = array( 'jpg', 'jpeg', 'png', 'webp' );
    if ( ! in_array( strtolower( $file_type['ext'] ), $allowed_types ) ) {
      wp_send_json_error( array( 'message' => 'Invalid file type. Only JPG, PNG, and WEBP are allowed.' ) );
      return;
    }

    // Validate file size (max 50MB)
    $file_size = isset( $_FILES['image']['size'] ) ? absint( $_FILES['image']['size'] ) : 0;
    if ( $file_size > 50 * 1024 * 1024 ) {
      wp_send_json_error( array( 'message' => 'File size must be less than 50MB' ) );
      return;
    }

    require_once( ABSPATH . 'wp-admin/includes/file.php' );
    require_once( ABSPATH . 'wp-admin/includes/media.php' );
    require_once( ABSPATH . 'wp-admin/includes/image.php' );

    $upload = wp_handle_upload( $_FILES['image'], array( 'test_form' => false ) );

    if ( isset( $upload['error'] ) ) {
      wp_send_json_error( array( 'message' => $upload['error'] ) );
      return;
    }

    $attachment = array(
      'post_mime_type' => $upload['type'],
      'post_title'     => sanitize_file_name( pathinfo( $file_name, PATHINFO_FILENAME ) ),
      'post_content'  => '',
      'post_status'   => 'inherit'
    );

    $attach_id = wp_insert_attachment( $attachment, $upload['file'] );
    $attach_data = wp_generate_attachment_metadata( $attach_id, $upload['file'] );
    wp_update_attachment_metadata( $attach_id, $attach_data );

    $image_url = wp_get_attachment_url( $attach_id );

    wp_send_json_success( array( 
      'attachment_id' => $attach_id,
      'url' => $image_url,
      'message' => 'Image uploaded successfully'
    ) );
  }

  /**
   * Create tour from wizard data
   * 
   * @since 8.5.48
   */
  public function wpvr_create_tour_from_wizard() {
    if ( ! current_user_can( 'edit_posts' ) ) {
      wp_send_json_error( array( 'message' => 'Unauthorized user' ), 403 );
      return;
    }

    $nonce = isset($_POST['security']) ? sanitize_text_field(wp_unslash( $_POST['security'] )) : '';
    if ( !wp_verify_nonce( $nonce, 'wpvr' ) ) {
      wp_send_json_error( array( 'message' => 'Invalid nonce' ), 400 );
      return;
    }

    $panodata = isset($_POST['panodata']) ? json_decode( wp_unslash( $_POST['panodata'] ), true ) : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
    $title = isset($_POST['title']) ? sanitize_text_field(wp_unslash( $_POST['title'] )) : 'My Virtual Tour';
    $industry = isset($_POST['industry']) ? sanitize_text_field(wp_unslash( $_POST['industry'] )) : 'real-estate';
    $existing_post_id = isset($_POST['existing_post_id']) ? absint(wp_unslash( $_POST['existing_post_id'] )) : 0;

    if ( empty( $panodata ) ) {
      wp_send_json_error( array( 'message' => 'Panodata is required' ) );
      return;
    }

    if ( $existing_post_id > 0 ) {
      $existing_post = get_post( $existing_post_id );
      if ( ! $existing_post || 'wpvr_item' !== $existing_post->post_type || ! current_user_can( 'edit_post', $existing_post_id ) ) {
        wp_send_json_error( array( 'message' => 'Invalid existing tour ID' ) );
        return;
      }

      $post_id = $existing_post_id;
      wp_update_post(
        array(
          'ID' => $post_id,
          'post_title' => $title,
          'post_status' => 'publish',
        )
      );
    } else {
      // Create new post
      $post_data = array(
        'post_title'   => $title,
        'post_status'  => 'publish',
        'post_type'    => 'wpvr_item',
        'post_author'  => get_current_user_id(),
      );

      $post_id = wp_insert_post( $post_data );

      if ( is_wp_error( $post_id ) ) {
        wp_send_json_error( array( 'message' => 'Failed to create tour: ' . $post_id->get_error_message() ) );
        return;
      }
    }

    // Enforce local media URLs before final save/update
    $panodata = $this->wpvr_import_scene_attachments_to_media( $panodata, $post_id );

    // Set panoid as pano{post_id} in panodata
    $panodata['panoid'] = 'pano' . $post_id;

    // Normalize autoLoad to boolean so Pannellum's strict === true check passes.
    if ( isset( $panodata['autoLoad'] ) ) {
      $panodata['autoLoad'] = (bool) $panodata['autoLoad'];
    }

    // Save panodata as post meta
    update_post_meta( $post_id, 'panodata', $panodata );

    // Mark as created from wizard
    update_post_meta( $post_id, 'wpvr_created_from_wizard', true );
    update_post_meta( $post_id, 'wpvr_wizard_industry', $industry );

    // Save template meta fields if provided (dynamic meta from API)
    $template_meta = isset($_POST['templateMeta']) ? json_decode( wp_unslash( $_POST['templateMeta'] ), true ) : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
    if ( ! empty( $template_meta ) && is_array( $template_meta ) ) {
      foreach ( $template_meta as $meta_key => $meta_value ) {
        // Sanitize meta key to ensure it's a valid meta key
        $sanitized_key = sanitize_key( $meta_key );
        if ( ! empty( $sanitized_key ) && 'panodata' !== $sanitized_key ) {
          // Handle different value types
          if ( is_array( $meta_value ) ) {
            update_post_meta( $post_id, $sanitized_key, $meta_value );
          } else {
            update_post_meta( $post_id, $sanitized_key, sanitize_text_field( $meta_value ) );
          }
        }
      }
    }

    // Trigger tour saved action for telemetry
    do_action('wpvr_rex_wpvr_tour_saved', $post_id);
    do_action( 'wpvr_setup_wizard_completed_event', $industry );

    // Persist industry selection for telemetry (aha event fires later from consent handler).
    update_option( 'wpvr_industry_name', sanitize_text_field( $industry ), false );

    // Mark wizard as permanently done so the onboarding notice is suppressed.
    update_option( 'wpvr_wizard_onboarding_done', '1', false );

    wp_send_json_success( array( 
      'post_id' => $post_id,
      'edit_url' => admin_url( 'post.php?action=edit&post=' . $post_id ),
      'view_url' => get_permalink( $post_id ),
      'message' => 'Tour created successfully'
    ) );
  }

}

```
