PluginProbe
WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress / 9.0.3
WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress v9.0.3
9.1.3 9.1.2 9.1.1 9.1.0 9.0.3 9.0.2 9.0.1 9.0.0 8.5.79 8.5.78 8.5.77 8.5.76 8.5.75 8.5.74 8.5.73 8.5.72 8.5.71 8.5.70 8.5.69 8.5.68 8.5.35 8.5.36 8.5.37 8.5.38 8.5.39 All 222 releases
wpvr / legacy / admin / classes / class-wpvr-ajax.php

class-wpvr-ajax.php in WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress 9.0.3, at legacy/admin/classes/class-wpvr-ajax.php

1,287 lines 45.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if (!defined('ABSPATH')) exit; // Exit if accessed directly
4 /**
5 * The admin-specific Ajax files.
6 *
7 * @link http://rextheme.com/
8 * @since 8.0.0
9 *
10 * @package Wpvr
11 * @subpackage Wpvr/admin
12 */
13
14 class Wpvr_Ajax
15 {
16
17 /**
18 * Instance of WPVR_Format class
19 *
20 * @var object
21 * @since 8.0.0
22 */
23 protected $format;
24
25
26 /**
27 * Instance of WPVR_StreetView class
28 *
29 * @var object
30 * @since 8.0.0
31 */
32 protected $streetview;
33
34
35 /**
36 * Instance of WPVR_Video class
37 *
38 * @var object
39 * @since 8.0.0
40 */
41 protected $video;
42
43
44 /**
45 * Instance of WPVR_Scene class
46 *
47 * @var object
48 * @since 8.0.0
49 */
50 protected $scene;
51
52
53 /**
54 * Instance of WPVR_Validator class
55 *
56 * @var object
57 * @since 8.0.0
58 */
59 protected $validator;
60
61
62 function __construct()
63 {
64 $this->format = new WPVR_Format();
65 $this->streetview = new WPVR_StreetView();
66 $this->video = new WPVR_Video();
67 $this->scene = new WPVR_Scene();
68 $this->validator = new WPVR_Validator();
69
70 add_action('wp_ajax_wpvr_save', array($this, 'wpvr_save_data'));
71 add_action('wp_ajax_wpvr_preview', array($this, 'wpvr_show_preview'));
72 add_action('wp_ajax_wpvrstreetview_preview', array($this, 'wpvrstreetview_preview'));
73 add_action('wp_ajax_wpvr_file_import', array($this, 'wpvr_file_import'));
74 add_action('wp_ajax_wpvr_role_management', array($this, 'wpvr_role_management'));
75 add_action('wp_ajax_wpvr_enable_large_image_handler', array($this, 'wpvr_enable_large_image_handler'));
76 add_action('wp_ajax_wpvr_notice', array($this, 'wpvr_notice'));
77 add_action('wp_ajax_wpvr_dismiss_black_friday_notice', array($this, 'dismiss_black_friday_notice'));
78 add_action('wp_ajax_wpvr_review_request', array($this, 'wpvr_review_request'));
79
80 //setup wizard ajax
81 add_action( 'wp_ajax_wpvr_create_contact', array($this, 'wpvr_create_contact' ) );
82
83 //general setting ajax
84 add_action( 'wp_ajax_wpvr_save_general_settings', array($this, 'wpvr_save_general_settings' ) );
85 // opt-in toggle ajax
86 add_action( 'wp_ajax_wpvr_save_opt_in_toggle', array($this, 'wpvr_save_opt_in_toggle' ) );
87
88 // Setup wizard specific AJAX handlers
89 add_action( 'wp_ajax_wpvr_fetch_template', array($this, 'wpvr_fetch_template' ) );
90 add_action( 'wp_ajax_wpvr_upload_image', array($this, 'wpvr_upload_image' ) );
91 add_action( 'wp_ajax_wpvr_create_tour_from_wizard', array($this, 'wpvr_create_tour_from_wizard' ) );
92 }
93
94 /**
95 * Enable full-resolution images without changing other settings.
96 *
97 * @return void
98 */
99 public function wpvr_enable_large_image_handler()
100 {
101 if (!current_user_can('manage_options')) {
102 wp_send_json_error(array('message' => 'Permission denied.'), 403);
103 }
104
105 $nonce = isset($_POST['nonce']) ? sanitize_text_field(wp_unslash($_POST['nonce'])) : '';
106
107 if (!wp_verify_nonce($nonce, 'wpvr')) {
108 wp_send_json_error(array('message' => 'Permission denied.'), 403);
109 }
110
111 update_option('high_res_image', 'true');
112
113 wp_send_json_success();
114 }
115
116 public function wpvr_review_request()
117 {
118 if( !current_user_can( 'manage_options' ) ){
119 wp_send_json_error( array( 'message' => 'Unauthorized user' ), 403 );
120 return;
121 }
122 $nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
123 if ( ! wp_verify_nonce( $nonce, 'wpvr-dismiss-notice-five-star-review' ) ) {
124 $response = array(
125 'success' => false,
126 'data' => 'Permission denied.'
127 );
128 wp_send_json($response);
129 }
130 $payload = !empty($_POST['payload']) ? map_deep( wp_unslash( $_POST['payload'] ), 'sanitize_text_field' ) : array();
131 $data = array(
132 'show' => !empty($payload['show']) ? $payload['show'] : '',
133 'time' => !empty($payload['frequency']) && 'never' !== $payload['frequency'] ? time() : '',
134 'frequency' => !empty($payload['frequency']) ? $payload['frequency'] : '',
135 );
136 update_option('wpvr_feed_review_request', $data);
137 $response = array(
138 'success' => true,
139 'data' => 'Review request updated successfully.'
140 );
141 wp_send_json($response);
142 die();
143 }
144
145 /**
146 * Responsible for Tour Preview
147 *
148 * @return void
149 * @since 8.0.0
150 */
151 public function wpvr_show_preview()
152 {
153 //===Current user capabilities check===//
154 if (!current_user_can('edit_posts')) {
155 $response = array(
156 'success' => false,
157 'data' => 'Contact admin.'
158 );
159 wp_send_json($response);
160 }
161 //===Current user capabilities check===//
162 //===Nonce check===//
163 $nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
164 if ( ! wp_verify_nonce( $nonce, 'wpvr' ) ) {
165 $response = array(
166 'success' => false,
167 'data' => 'Permission denied.'
168 );
169 wp_send_json($response);
170 }
171 //===Nonce check===//
172
173 $panoid = '';
174 $postid = isset( $_POST['postid'] ) ? sanitize_text_field( wp_unslash( $_POST['postid'] ) ) : 0;
175 $panoid = 'pano' . $postid;
176 $panovideo = 'off';
177 if ( isset( $_POST['panovideo'] ) ) {
178 $panovideo = sanitize_text_field( wp_unslash( $_POST['panovideo'] ) );
179 }
180
181 $post_type = get_post_type($postid);
182 if ($post_type != 'wpvr_item' || ! current_user_can('edit_post', $postid)) {
183 die();
184 }
185
186 do_action('wpvr_pro_street_view_preview', $postid, $panoid);
187
188 if ($panovideo == 'off') {
189 $this->scene->wpvr_scene_preview($panoid, $panovideo); // Preapre preview based on Scene data //
190 } else {
191 $this->video->wpvr_video_preview($panoid); // Prepare preview based on Video data //
192 }
193 }
194
195
196 /**
197 * Responsible for saving WPVR data
198 *
199 * @return void
200 * @since 8.0.0
201 */
202 public function wpvr_save_data()
203 {
204 /**
205 * Verify current user has permission to perform this action.
206 *
207 * @return void
208 */
209 if ( ! current_user_can('edit_posts') ) {
210 wp_send_json([
211 'success' => false,
212 'data' => 'Permission denied.'
213 ]);
214 }
215
216 /**
217 * Validate AJAX nonce to prevent unauthorized or forged requests.
218 *
219 * @return void
220 */
221 $nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
222 if ( ! wp_verify_nonce( $nonce, 'wpvr' ) ) {
223 wp_send_json([
224 'success' => false,
225 'data' => 'Invalid or expired request.',
226 ]);
227 }
228
229
230 $postid = isset( $_POST['postid'] ) ? absint( sanitize_text_field( wp_unslash( $_POST['postid'] ) ) ) : 0;
231
232 /**
233 * Ensures a valid post ID is supplied before proceeding.
234 *
235 * @return void
236 */
237 if($postid < 1) {
238 wp_send_json_error([
239 'success' => false,
240 'data' => '<span class="pano-error-title">Invalid post ID</span> <p>Malformed data passed.</p>'
241 ]);
242 die();
243 }
244
245 /**
246 * Ensures the post type is 'wpvr_item' before proceeding.
247 *
248 * @return void
249 */
250 $post_type = get_post_type( $postid );
251 if ($post_type != 'wpvr_item' || ! current_user_can( 'edit_post', $postid )) {
252 wp_send_json([
253 'success' => false,
254 'data' => 'Permission denied.'
255 ]);
256 }
257
258 $panoid = 'pano' . $postid;
259
260
261 /**
262 * Checks if this is a publish action and validates scene/video data.
263 *
264 * @return void
265 */
266 $action_type = isset($_POST['action_type']) ? sanitize_text_field(wp_unslash( $_POST['action_type'] )) : 'auto-draft';
267 $is_publish_action = ($action_type === 'publish');
268
269 /**
270 * Checks if title is provided FIRST before any other validation.
271 *
272 * @return void
273 */
274 if ($is_publish_action && (!isset($_POST['post_title']) || empty(trim(sanitize_text_field(wp_unslash( $_POST['post_title'] )))))) {
275 wp_send_json([
276 'success' => false,
277 'data' => '<span class="pano-error-title">Title Required!</span> <p>Please provide a title for this tour before publishing.</p>'
278 ]);
279 die();
280 }
281
282 /**
283 * Validates scene/video data before allowing publication.
284 *
285 * @return void
286 */
287 $is_street_view_mode = (!empty($_POST['streetview']) && sanitize_text_field(wp_unslash( $_POST['streetview'] )) == 'on');
288
289 if ($is_publish_action) {
290
291 $has_scene_data = false;
292 $has_video_data = false;
293 $is_video_mode = false;
294 $has_street_view_data = false;
295
296 // Check if video mode is enabled
297 if (isset($_POST['panovideo']) && sanitize_text_field(wp_unslash( $_POST['panovideo'] )) === 'on') {
298 $is_video_mode = true;
299 if (isset($_POST['videourl']) && !empty($_POST['videourl'])) {
300 $has_video_data = true;
301 }
302 } elseif (!empty($_POST['streetview']) && sanitize_text_field(wp_unslash( $_POST['streetview'] )) == 'on') {
303 // Check if Street View mode is enabled (Pro feature)
304 $is_street_view_mode = true;
305 if (!empty($_POST['streetviewurl'])) {
306 $has_street_view_data = true;
307 }
308 // Street View doesn't require scene data as it uses Google Street View API
309 } else {
310 // Check for scene data
311 if (isset($_POST['panodata']) && !empty($_POST['panodata'])) {
312 $panodata = json_decode( wp_unslash( $_POST['panodata'] ), true ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
313 if (isset($panodata['scene-list']) && !empty($panodata['scene-list'])) {
314 foreach ($panodata['scene-list'] as $scene) {
315 // Check if it's a cubemap scene
316 if (isset($scene['scene-type']) && $scene['scene-type'] === 'cubemap') {
317 // Check all six faces of the cube
318 $required_faces = array(
319 'scene-attachment-url-face0',
320 'scene-attachment-url-face1',
321 'scene-attachment-url-face2',
322 'scene-attachment-url-face3',
323 'scene-attachment-url-face4',
324 'scene-attachment-url-face5'
325 );
326
327 $missing_faces = array();
328 foreach ($required_faces as $face) {
329 if (empty($scene[$face])) {
330 $missing_faces[] = $face;
331 }
332 }
333
334 if (!empty($missing_faces)) {
335 $response = array(
336 'success' => false,
337 '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>'
338 );
339 wp_send_json($response);
340 die();
341 }
342
343 if (!empty($scene['scene-id'])) {
344 $has_scene_data = true;
345 }
346 } else {
347 // Regular equirectangular scene check
348 if (!empty($scene['scene-id']) && !empty($scene['scene-attachment-url'])) {
349 $has_scene_data = true;
350 break;
351 }
352 }
353 }
354 }
355 }
356 }
357
358 // Provide specific error messages based on the mode and missing data
359 if ($is_video_mode && !$has_video_data) {
360 // Video mode is enabled but no video URL provided
361 $response = array(
362 'success' => false,
363 '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>'
364 );
365 wp_send_json($response);
366 die();
367 } elseif($is_street_view_mode && !$has_street_view_data) {
368 $response = array(
369 'success' => false,
370 '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>'
371 );
372 wp_send_json($response);
373 die();
374 }elseif (!$is_video_mode && !$is_street_view_mode && !$has_scene_data) {
375 // Scene mode but no valid scenes found (exclude Street View from this check)
376 $response = array(
377 'success' => false,
378 '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>'
379 );
380 wp_send_json($response);
381 die();
382 }
383 }
384
385 $post_array = array(
386 'post_status' => get_post_status( $postid ),
387 'post_password' => get_post_field( 'post_password', $postid ),
388 'visibility' => 'public',
389 );
390
391 if ( isset( $_POST['post_status'] ) ) {
392 $post_status = sanitize_text_field( wp_unslash( $_POST['post_status'] ) );
393 $post_array['post_status'] = $post_status;
394 }
395 if ( isset( $_POST['post_password'] ) ) {
396 $post_password = sanitize_text_field( wp_unslash( $_POST['post_password'] ) );
397 $post_array['post_password'] = $post_password;
398 }
399 if ( isset( $_POST['visibility'] ) ) {
400 $visibility = sanitize_text_field( wp_unslash( $_POST['visibility'] ) );
401 $post_array['visibility'] = $visibility;
402 if ( $visibility == 'public' || $visibility == 'private' ) {
403 $post_array['post_password'] = '';
404 }
405 }
406
407 if ( $post_array['visibility'] == 'private' ) {
408 $post_array['post_status'] = 'private';
409 } elseif ( $is_publish_action ) {
410 $post_array['post_status'] = 'publish';
411 } else {
412 // Keep current status or set to draft if it's auto-draft
413 $current_status = get_post_status( $postid );
414 if ( $current_status === 'auto-draft' ) {
415 $post_array['post_status'] = 'draft';
416 }
417 }
418
419 $post_title = isset( $_POST['post_title'] ) ? sanitize_text_field( wp_unslash( $_POST['post_title'] ) ) : get_the_title( $postid );
420 wp_update_post( array(
421 'ID' => $postid,
422 'post_status' => $post_array['post_status'],
423 'post_password' => $post_array['post_password'],
424 'post_title' => $post_title,
425 ) );
426
427 do_action( 'wpvr_pro_update_street_view', $postid, $panoid );
428
429 if ( isset( $_POST['checklistData'] ) && !empty( $_POST['checklistData'] ) ) {
430 $checklist_data = array_map( 'sanitize_text_field', wp_unslash( $_POST['checklistData'] ) );
431 update_post_meta( $postid, 'wpvr_checklist', $checklist_data );
432 }
433 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
434 error_log("Tour with ID $postid has been saved with status " . $post_array['post_status']);
435 }
436
437 if ( ! $is_street_view_mode ) {
438 if ( isset( $_POST['panovideo'] ) && sanitize_text_field( wp_unslash( $_POST['panovideo'] ) ) == 'on' ) {
439 $this->video->wpvr_update_meta_box( $postid, $panoid, $is_publish_action );
440 } else {
441 $this->scene->wpvr_update_meta_box( $postid, $panoid, $is_publish_action );
442 }
443 }
444
445 do_action('wpvr_rex_wpvr_tour_saved', $postid);
446
447 $response = array(
448 'success' => true,
449 'data' => array(
450 'post_ID' => $postid,
451 'post_status' => get_post_status($postid)
452 )
453 );
454 wp_send_json($response);
455 die();
456 }
457
458
459 /**
460 * Responsible for importing tour
461 *
462 * @return void
463 * @since 8.0.0
464 */
465 public function wpvr_file_import()
466 {
467 //===Current user capabilities check===//
468 if (!current_user_can('edit_posts')) {
469 $response = array(
470 'success' => false,
471 'data' => 'Permission denied.'
472 );
473 wp_send_json($response);
474 }
475 //===Current user capabilities check===//
476 //===Nonce check===//
477 $nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
478 if ( ! wp_verify_nonce( $nonce, 'wpvr' ) ) {
479 $response = array(
480 'success' => false,
481 'data' => 'Permission denied.'
482 );
483 wp_send_json($response);
484 }
485 $file_name = '';
486
487 if (
488 isset( $_FILES['wpvr_import_file'] )
489 && $_FILES['wpvr_import_file']['error'] === UPLOAD_ERR_OK
490 && ! empty( $_FILES['wpvr_import_file']['tmp_name'] )
491 ) {
492 $file = $_FILES['wpvr_import_file'];
493
494 // Validate file type - check if it's a ZIP file
495 $file_type = wp_check_filetype($file['name']);
496 $file_ext = strtolower($file_type['ext']);
497 if ($file_ext !== 'zip') {
498 wp_send_json_error(array('message' => 'Invalid file format. Only ZIP files are allowed.'));
499 return;
500 }
501
502 // Define a temporary filter to change the upload directory to our temp folder
503 $upload_dir_filter = function( $upload ) {
504 $upload['subdir'] = '/wpvr_imported_temp';
505 $upload['path'] = $upload['basedir'] . $upload['subdir'];
506 $upload['url'] = $upload['baseurl'] . $upload['subdir'];
507 return $upload;
508 };
509
510 // Apply the filter
511 add_filter( 'upload_dir', $upload_dir_filter );
512
513 // Use wp_handle_upload to securely handle the uploaded file
514 $upload_overrides = array( 'test_form' => false );
515 $movefile = wp_handle_upload( $file, $upload_overrides );
516
517 // Remove the filter immediately after upload
518 remove_filter( 'upload_dir', $upload_dir_filter );
519
520 if ( $movefile && ! isset( $movefile['error'] ) ) {
521 // Use the sanitized file name generated by WordPress
522 $file_name = basename( $movefile['file'] );
523 } else {
524 wp_send_json_error( array( 'message' => $movefile['error'] ) );
525 return;
526 }
527
528 } else {
529 $upload_error = isset( $_FILES['wpvr_import_file']['error'] ) ? (int) $_FILES['wpvr_import_file']['error'] : -1;
530 $error_messages = array(
531 UPLOAD_ERR_INI_SIZE => 'File exceeds the server upload size limit (upload_max_filesize).',
532 UPLOAD_ERR_FORM_SIZE => 'File exceeds the form upload size limit.',
533 UPLOAD_ERR_PARTIAL => 'File was only partially uploaded.',
534 UPLOAD_ERR_NO_FILE => 'No file selected.',
535 UPLOAD_ERR_NO_TMP_DIR => 'Missing temporary folder.',
536 UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk.',
537 UPLOAD_ERR_EXTENSION => 'A PHP extension stopped the upload.',
538 );
539 $message = isset( $error_messages[ $upload_error ] ) ? $error_messages[ $upload_error ] : 'No file selected.';
540 wp_send_json_error( array( 'message' => $message ) );
541 return;
542 }
543
544 //===Nonce check===//
545 WPVR_Import::prepare_tour_import_feature($file_name);
546 }
547
548
549
550 /**
551 * WPVR Role Management
552 *
553 * @return void
554 * @since 8.0.0
555 */
556 function wpvr_role_management()
557 {
558
559 //===Current user capabilities check===//
560 if (!current_user_can('manage_options')) {
561 $response = array(
562 'success' => false,
563 'data' => 'Permission denied.'
564 );
565 wp_send_json($response);
566 }
567 //===Current user capabilities check===//
568 //===Nonce check===//
569 $nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
570 if ( ! wp_verify_nonce( $nonce, 'wpvr' ) ) {
571 $response = array(
572 'success' => false,
573 'data' => 'Permission denied.'
574 );
575 wp_send_json($response);
576 }
577 //===Nonce check===//
578
579 $editor = isset( $_POST['editor'] ) ? sanitize_text_field( wp_unslash( $_POST['editor'] ) ) : '';
580 $author = isset( $_POST['author'] ) ? sanitize_text_field( wp_unslash( $_POST['author'] ) ) : '';
581 $fontawesome = isset( $_POST['fontawesome'] ) ? sanitize_text_field( wp_unslash( $_POST['fontawesome'] ) ) : '';
582
583 $cardboard = !empty($_POST['wpvr_cardboard_disable']) ? sanitize_text_field(wp_unslash( $_POST['wpvr_cardboard_disable'] )) : 'no'; //
584
585 $wpvr_webp_conversion = !empty($_POST['wpvr_webp_conversion']) ? sanitize_text_field(wp_unslash( $_POST['wpvr_webp_conversion'] )) : 'no';
586
587 $mobile_media_resize = isset( $_POST['mobile_media_resize'] ) ? sanitize_text_field( wp_unslash( $_POST['mobile_media_resize'] ) ) : '';
588 $high_res_image = isset( $_POST['high_res_image'] ) ? sanitize_text_field( wp_unslash( $_POST['high_res_image'] ) ) : '';
589 $dis_on_hover = isset( $_POST['dis_on_hover'] ) ? sanitize_text_field( wp_unslash( $_POST['dis_on_hover'] ) ) : '';
590 $wpvr_mobile_hotspot_tip = isset( $_POST['wpvr_mobile_hotspot_tip'] ) ? sanitize_text_field( wp_unslash( $_POST['wpvr_mobile_hotspot_tip'] ) ) : '';
591 $wpvr_frontend_notice = isset( $_POST['wpvr_frontend_notice'] ) ? sanitize_text_field( wp_unslash( $_POST['wpvr_frontend_notice'] ) ) : '';
592 $wpvr_frontend_notice_area = isset( $_POST['wpvr_frontend_notice_area'] ) ? sanitize_text_field( wp_unslash( $_POST['wpvr_frontend_notice_area'] ) ) : '';
593 $wpvr_script_control = isset( $_POST['wpvr_script_control'] ) ? sanitize_text_field( wp_unslash( $_POST['wpvr_script_control'] ) ) : '';
594 $wpvr_script_list = isset( $_POST['wpvr_script_list'] ) ? sanitize_text_field( wp_unslash( $_POST['wpvr_script_list'] ) ) : '';
595
596 $wpvr_video_script_control = isset( $_POST['wpvr_video_script_control'] ) ? sanitize_text_field( wp_unslash( $_POST['wpvr_video_script_control'] ) ) : '';
597 $wpvr_video_script_list = isset( $_POST['wpvr_video_script_list'] ) ? sanitize_text_field( wp_unslash( $_POST['wpvr_video_script_list'] ) ) : '';
598
599 // $enable_woocommerce = sanitize_text_field(wp_unslash( $_POST['woocommerce'] ));
600
601 $wpvr_script_list = str_replace(' ', '', $wpvr_script_list);
602
603 update_option('wpvr_editor_active', $editor);
604 update_option('wpvr_author_active', $author);
605 update_option('wpvr_fontawesome_disable', $fontawesome);
606 update_option('wpvr_cardboard_disable', $cardboard);
607 update_option('wpvr_webp_conversion', $wpvr_webp_conversion);
608 update_option('mobile_media_resize', $mobile_media_resize);
609 update_option('high_res_image', $high_res_image);
610 update_option('dis_on_hover', $dis_on_hover);
611 update_option('wpvr_mobile_hotspot_tip', $wpvr_mobile_hotspot_tip ? 'true' : 'false');
612 update_option('wpvr_frontend_notice', $wpvr_frontend_notice);
613 update_option('wpvr_frontend_notice_area', $wpvr_frontend_notice_area);
614 update_option('wpvr_script_control', $wpvr_script_control);
615 update_option('wpvr_script_list', $wpvr_script_list);
616
617 update_option('wpvr_video_script_control', $wpvr_video_script_control);
618 update_option('wpvr_video_script_list', $wpvr_video_script_list);
619
620 if(is_plugin_active( 'dokan-lite/dokan.php' ) || is_plugin_active( 'dokan-pro/dokan.php' )){
621 $dokan_vendor = isset( $_POST['dokan_vendor'] ) ? sanitize_text_field(wp_unslash( $_POST['dokan_vendor'] )) : false;
622 update_option('dokan_vendor_active', $dokan_vendor);
623 }
624
625 // Usage data sharing toggle — sync with Linno telemetry SDK.
626 if ( isset( $_POST['wpvr_usage_tracking'] ) ) {
627 $tracking_toggle = sanitize_text_field( wp_unslash( $_POST['wpvr_usage_tracking'] ) );
628 $consent_state = 'true' === $tracking_toggle ? 'yes' : 'no';
629 $opt_in_numeric = 'yes' === $consent_state ? '1' : '0';
630
631 update_option( 'wpvr_allow_tracking', $consent_state );
632 update_option( 'wpvr_opt_in_toggle', $opt_in_numeric );
633
634 if ( function_exists( 'linno_telemetry' ) && defined( 'WPVR_FILE' ) ) {
635 $telemetry_client = linno_telemetry( WPVR_FILE );
636 if ( $telemetry_client && method_exists( $telemetry_client, 'set_optin_state' ) ) {
637 $telemetry_client->set_optin_state( $consent_state );
638 } elseif ( function_exists( 'linno_telemetry_sync_consent_state' ) ) {
639 linno_telemetry_sync_consent_state( WPVR_FILE );
640 }
641 }
642
643 if ( 'yes' === $consent_state ) {
644 do_action( 'wpvr_telemetry_consent_granted' );
645 }
646 }
647
648 // update_option('wpvr_enable_woocommerce', $enable_woocommerce);
649
650 $response = array(
651 'status' => 'success',
652 'message' => 'Successfully saved',
653 );
654 wp_send_json($response);
655 }
656
657
658 /**
659 * WPVR Notice
660 *
661 * @return void
662 * @since 8.0.0
663 */
664 function wpvr_notice()
665 {
666 //===Current user capabilities check===//
667 if (!current_user_can('manage_options')) {
668 $response = array(
669 'success' => false,
670 'data' => 'Permission denied.'
671 );
672 wp_send_json($response);
673 }
674 //===Current user capabilities check===//
675 //===Nonce check===//
676 $nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
677 if ( ! wp_verify_nonce( $nonce, 'wpvr' ) ) {
678 $response = array(
679 'success' => false,
680 'data' => 'Permission denied.'
681 );
682 wp_send_json($response);
683 }
684 //===Nonce check===//
685 update_option('wpvr_black_friday_notice', '1');
686 }
687
688 /**
689 * Dismiss black friday notice
690 */
691 function dismiss_black_friday_notice(){
692 if( !current_user_can( 'manage_options' ) ){
693 wp_send_json_error( array( 'message' => 'Unauthorized user' ), 403 );
694 return;
695 }
696 if (!isset($_POST['nonce']) || !wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'wpvr')) {
697 wp_die(esc_html__('Permission check failed', 'wpvr'));
698 }
699 update_option('_wpvr_eid_al_adha_2024', 'yes');
700 echo json_encode(['success' => true,]);
701 wp_die();
702 }
703
704 /**
705 * Handles the creation of a contact via a webhook.
706 *
707 * This function validates the nonce, sanitizes and validates the input fields,
708 * and then creates a new contact using the WPVR_Create_Contact class.
709 *
710 * @since 8.4.10
711 */
712 function wpvr_create_contact(){
713 if( !current_user_can( 'manage_options' ) ){
714 wp_send_json_error( array( 'message' => 'Unauthorized user' ), 403 );
715 return;
716 }
717 $nonce = filter_input(INPUT_POST, 'security', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
718 $nonce = !empty( $nonce ) ? $nonce : null;
719 if ( !wp_verify_nonce( $nonce, 'wpvr' ) ) {
720 wp_send_json_error( array( 'message' => 'Invalid nonce' ), 400 );
721 return;
722 }
723
724 $name = filter_input(INPUT_POST, 'name', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
725 $industry = filter_input(INPUT_POST, 'industry', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
726 $email = filter_input(INPUT_POST, 'email', FILTER_SANITIZE_EMAIL);
727 $opt_in = filter_input(INPUT_POST, 'opt_in', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
728
729 $name = !empty($name) ? $name: '';
730 $industry = !empty($industry ) ? $industry : '';
731 $email = !empty( $email ) ? $email : '';
732
733 if ( empty( $email ) ) {
734 wp_send_json_error( array( 'message' => __('Email is required', 'wpvr') ), 400 );
735 }elseif( !is_email( $email ) ){
736 wp_send_json_error( array( 'message' => __('Email is invalid', 'wpvr') ), 400 );
737 }
738
739 $create_contact_instance = new WPVR_Create_Contact( $email, $name, $industry );
740 $response = $create_contact_instance->create_contact_via_webhook();
741
742 update_option('wpvr_posthog_access_enabled', $opt_in);
743
744
745 if ( $response ) {
746 wp_send_json_success( array( 'message' => __('Contact created successfully', 'wpvr') ), 200 );
747 } else {
748 wp_send_json_error( array( 'message' => __('Failed to create contact', 'wpvr') ), 500 );
749 }
750 }
751
752 /**
753 * Saves the general settings for the WPVR plugin.
754 *
755 * This function handles the nonce verification, sanitizes the input fields,
756 * and updates the options in the database. It responds with a JSON success or error message.
757 *
758 * @since 8.4.10
759 */
760 function wpvr_save_general_settings(){
761
762 if ( ! current_user_can( 'manage_options' ) ) {
763 wp_send_json_error( array( 'message' => 'Unauthorized user' ), 403 );
764 return;
765 }
766
767 $nonce = filter_input(INPUT_POST, 'security', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
768 $nonce = !empty( $nonce ) ? $nonce : null; // phpcs:ignore
769 if ( !wp_verify_nonce( $nonce, 'wpvr' ) ) {
770 wp_send_json_error( array( 'message' => 'Invalid nonce' ), 400 );
771 return;
772 }
773
774 $is_mobile_media_resize = filter_input(INPUT_POST, 'media_resizer', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
775 $convert_to_webp = filter_input(INPUT_POST, 'convert_to_webp', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
776 $vr_glass_support = filter_input(INPUT_POST, 'vr_glass_support', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
777
778 update_option('mobile_media_resize', $is_mobile_media_resize);
779 update_option('wpvr_webp_conversion', $convert_to_webp);
780 update_option('wpvr_cardboard_disable', $vr_glass_support);
781
782 wp_send_json_success( array( 'message' => __('General setting data successfully saved.', 'wpvr') ), 200 );
783 }
784
785
786 /**
787 * AJAX handler to persist opt-in toggle value
788 *
789 */
790 public function wpvr_save_opt_in_toggle() {
791 if ( ! current_user_can( 'manage_options' ) ) {
792 wp_send_json_error( array( 'message' => 'Unauthorized user' ), 403 );
793 return;
794 }
795
796 $nonce = isset($_POST['security']) ? sanitize_text_field(wp_unslash( $_POST['security'] )) : '';
797 if ( !wp_verify_nonce( $nonce, 'wpvr' ) ) {
798 wp_send_json_error( array( 'message' => 'Invalid nonce' ), 400 );
799 return;
800 }
801
802 $opt_in = isset($_POST['opt_in']) ? sanitize_text_field(wp_unslash( $_POST['opt_in'] )) : '0';
803 $consent_state = '1' === $opt_in ? 'yes' : 'no';
804
805 update_option('wpvr_opt_in_toggle', $opt_in);
806 update_option('wpvr_allow_tracking', $consent_state);
807
808 if ( 'yes' === $consent_state ) {
809 $this->wpvr_create_contact_for_current_user();
810 }
811
812 if ( function_exists( 'linno_telemetry' ) && defined( 'WPVR_FILE' ) ) {
813 $telemetry_client = linno_telemetry( WPVR_FILE );
814
815 if ( $telemetry_client && method_exists( $telemetry_client, 'set_optin_state' ) ) {
816 $telemetry_client->set_optin_state( $consent_state );
817 } elseif ( function_exists( 'linno_telemetry_sync_consent_state' ) ) {
818 linno_telemetry_sync_consent_state( WPVR_FILE );
819 }
820 }
821
822 // Fire after SDK consent is fully synced so consent-gated events can queue.
823 if ( 'yes' === $consent_state ) {
824 do_action( 'wpvr_telemetry_consent_granted' );
825 }
826
827 wp_send_json_success( array( 'message' => __('Opt-in value saved.', 'wpvr') ), 200 );
828 }
829
830
831 /**
832 * Create webhook contact from current user after consent.
833 *
834 * @return void
835 */
836 private function wpvr_create_contact_for_current_user() {
837 $current_user = wp_get_current_user();
838 if ( ! $current_user || empty( $current_user->user_email ) ) {
839 return;
840 }
841
842 $email = sanitize_email( $current_user->user_email );
843 if ( ! is_email( $email ) ) {
844 return;
845 }
846
847 $name = sanitize_text_field( $current_user->display_name );
848 if ( empty( $name ) ) {
849 $name = sanitize_text_field( $current_user->user_login );
850 }
851
852 $industry = sanitize_text_field( get_option( 'wpvr_industry_name', '' ) );
853
854 $create_contact_instance = new WPVR_Create_Contact( $email, $name, $industry );
855 $create_contact_instance->create_contact_via_webhook();
856 }
857
858
859 /**
860 * Fetch template tour object from remote API
861 *
862 * @since 8.5.48
863 */
864 public function wpvr_fetch_template() {
865 if ( ! current_user_can( 'manage_options' ) ) {
866 wp_send_json_error( array( 'message' => 'Unauthorized user' ), 403 );
867 return;
868 }
869
870 $nonce = isset($_POST['security']) ? sanitize_text_field(wp_unslash( $_POST['security'] )) : '';
871 if ( !wp_verify_nonce( $nonce, 'wpvr' ) ) {
872 wp_send_json_error( array( 'message' => 'Invalid nonce' ), 400 );
873 return;
874 }
875
876 $industry = isset($_POST['industry']) ? sanitize_text_field(wp_unslash( $_POST['industry'] )) : 'real-estate';
877
878 // Static industry to remote tour ID mapping
879 $industry_id_map = array(
880 'exhibitions' => 2140,
881 'offices' => 2145,
882 'real-estate' => 2147,
883 'hotel' => 2149,
884 'ecommerce' => 2151,
885 'showrooms' => 2153,
886 'school' => 2155,
887 );
888
889 // Get source tour ID for the selected industry
890 $source_tour_id = isset($industry_id_map[$industry]) ? $industry_id_map[$industry] : 2147;
891
892 // Build API URL with source tour ID
893 $api_url = 'https://showcase.rextheme.com/wp-json/wpvr/v1/tour/' . intval($source_tour_id);
894 $api_url = apply_filters('wpvr_template_api_url', $api_url, $industry, $source_tour_id);
895 $response = wp_remote_get($api_url, array(
896 'timeout' => 30,
897 'headers' => array(
898 'Content-Type' => 'application/json',
899 'Accept' => 'application/json',
900 ),
901 ));
902
903 if ( is_wp_error( $response ) ) {
904 wp_send_json_error( array( 'message' => 'Failed to fetch template: ' . $response->get_error_message() ) );
905 return;
906 }
907
908 $status_code = wp_remote_retrieve_response_code( $response );
909 if ( $status_code !== 200 ) {
910 wp_send_json_error( array( 'message' => 'Template not found (HTTP ' . $status_code . ')' ) );
911 return;
912 }
913
914 $body = wp_remote_retrieve_body( $response );
915 $api_data = json_decode( $body, true );
916
917 if ( ! $api_data || ! is_array( $api_data ) ) {
918 wp_send_json_error( array( 'message' => 'Invalid template data received' ) );
919 return;
920 }
921
922 $remote_meta = array();
923 if ( isset( $api_data['meta_data'] ) && is_array( $api_data['meta_data'] ) ) {
924 $remote_meta = $api_data['meta_data'];
925 } elseif ( isset( $api_data['meta'] ) && is_array( $api_data['meta'] ) ) {
926 $remote_meta = $api_data['meta'];
927 }
928
929 $panodata = array();
930 if ( isset( $remote_meta['panodata'] ) ) {
931 $panodata = $this->wpvr_normalize_panodata( $remote_meta['panodata'] );
932 }
933 if ( empty( $panodata ) && isset( $api_data['panodata'] ) ) {
934 $panodata = $this->wpvr_normalize_panodata( $api_data['panodata'] );
935 }
936
937 if ( empty( $panodata ) ) {
938 wp_send_json_error( array( 'message' => 'Template panodata missing in API response' ) );
939 return;
940 }
941
942 $title = isset( $api_data['title'] ) && ! empty( $api_data['title'] )
943 ? sanitize_text_field( $api_data['title'] )
944 : 'My Virtual Tour';
945
946 $post_data = array(
947 'post_title' => $title,
948 'post_status' => 'publish',
949 'post_type' => 'wpvr_item',
950 'post_author' => get_current_user_id(),
951 );
952
953 $post_id = wp_insert_post( $post_data );
954 if ( is_wp_error( $post_id ) ) {
955 wp_send_json_error( array( 'message' => 'Failed to create tour: ' . $post_id->get_error_message() ) );
956 return;
957 }
958
959 $panodata = $this->wpvr_import_scene_attachments_to_media( $panodata, $post_id );
960 $panodata['panoid'] = 'pano' . $post_id;
961
962 // Keep meta panodata in sync with imported local scene URLs
963 if ( ! is_array( $remote_meta ) ) {
964 $remote_meta = array();
965 }
966 $remote_meta['panodata'] = $panodata;
967
968 update_post_meta( $post_id, 'panodata', $panodata );
969 update_post_meta( $post_id, 'wpvr_created_from_wizard', true );
970 update_post_meta( $post_id, 'wpvr_wizard_industry', $industry );
971
972 if ( ! empty( $remote_meta ) ) {
973 foreach ( $remote_meta as $meta_key => $meta_value ) {
974 $sanitized_key = sanitize_key( $meta_key );
975 if ( empty( $sanitized_key ) || 'panodata' === $sanitized_key ) {
976 continue;
977 }
978
979 if ( is_array( $meta_value ) ) {
980 update_post_meta( $post_id, $sanitized_key, $meta_value );
981 } else {
982 update_post_meta( $post_id, $sanitized_key, sanitize_text_field( $meta_value ) );
983 }
984 }
985 }
986
987 $template_data = array(
988 'industry' => $industry,
989 'template_id' => $source_tour_id,
990 'post_id' => $post_id,
991 'edit_url' => admin_url( 'post.php?action=edit&post=' . $post_id ),
992 'view_url' => get_permalink( $post_id ),
993 'panodata' => $panodata,
994 'meta' => $remote_meta,
995 );
996
997 if ( isset( $panodata['panodata']['scene-list']['1']['scene-attachment-url'] ) ) {
998 $template_data['image_url'] = esc_url_raw( $panodata['panodata']['scene-list']['1']['scene-attachment-url'] );
999 } elseif ( isset( $api_data['image_url'] ) ) {
1000 $template_data['image_url'] = esc_url_raw( $api_data['image_url'] );
1001 } elseif ( isset( $api_data['featured_image'] ) ) {
1002 $template_data['image_url'] = esc_url_raw( $api_data['featured_image'] );
1003 }
1004
1005 do_action('wpvr_rex_wpvr_tour_saved', $post_id);
1006
1007 wp_send_json_success( array( 'template' => $template_data ) );
1008 }
1009
1010 /**
1011 * Import scene attachment URLs into media library and replace URLs in panodata.
1012 *
1013 * @param array $panodata Panodata structure.
1014 * @param int $post_id Target post ID.
1015 *
1016 * @return array
1017 */
1018 private function wpvr_import_scene_attachments_to_media( $panodata, $post_id ) {
1019 if ( empty( $panodata['panodata']['scene-list'] ) || ! is_array( $panodata['panodata']['scene-list'] ) ) {
1020 return $panodata;
1021 }
1022
1023 require_once( ABSPATH . 'wp-admin/includes/file.php' );
1024 require_once( ABSPATH . 'wp-admin/includes/media.php' );
1025 require_once( ABSPATH . 'wp-admin/includes/image.php' );
1026
1027 $scene_image_keys = array(
1028 'scene-attachment-url',
1029 'scene-attachment-url-face0',
1030 'scene-attachment-url-face1',
1031 'scene-attachment-url-face2',
1032 'scene-attachment-url-face3',
1033 'scene-attachment-url-face4',
1034 'scene-attachment-url-face5',
1035 );
1036
1037 $imported_urls = array();
1038
1039 foreach ( $panodata['panodata']['scene-list'] as $scene_key => $scene ) {
1040 if ( ! is_array( $scene ) ) {
1041 continue;
1042 }
1043
1044 foreach ( $scene_image_keys as $image_key ) {
1045 if ( empty( $scene[ $image_key ] ) || ! is_string( $scene[ $image_key ] ) ) {
1046 continue;
1047 }
1048
1049 $source_url = esc_url_raw( $scene[ $image_key ] );
1050 if ( empty( $source_url ) ) {
1051 continue;
1052 }
1053
1054 if ( isset( $imported_urls[ $source_url ] ) ) {
1055 $panodata['panodata']['scene-list'][ $scene_key ][ $image_key ] = $imported_urls[ $source_url ];
1056 continue;
1057 }
1058
1059 $attachment_id = attachment_url_to_postid( $source_url );
1060 if ( ! $attachment_id ) {
1061 $attachment_id = media_sideload_image( $source_url, $post_id, null, 'id' );
1062 if ( is_wp_error( $attachment_id ) ) {
1063 continue;
1064 }
1065 }
1066
1067 $local_url = wp_get_attachment_url( $attachment_id );
1068 if ( ! empty( $local_url ) ) {
1069 $local_url = esc_url_raw( $local_url );
1070 $imported_urls[ $source_url ] = $local_url;
1071 $panodata['panodata']['scene-list'][ $scene_key ][ $image_key ] = $local_url;
1072 }
1073 }
1074 }
1075
1076 return $panodata;
1077 }
1078
1079 /**
1080 * Normalize panodata payloads from array/serialized/json values.
1081 *
1082 * @param mixed $raw_panodata Panodata from remote API/meta.
1083 *
1084 * @return array
1085 */
1086 private function wpvr_normalize_panodata( $raw_panodata ) {
1087 if ( is_array( $raw_panodata ) ) {
1088 return $raw_panodata;
1089 }
1090
1091 if ( is_string( $raw_panodata ) && '' !== $raw_panodata ) {
1092 $unserialized = maybe_unserialize( $raw_panodata );
1093 if ( is_array( $unserialized ) ) {
1094 return $unserialized;
1095 }
1096
1097 $decoded_json = json_decode( $raw_panodata, true );
1098 if ( is_array( $decoded_json ) ) {
1099 return $decoded_json;
1100 }
1101 }
1102
1103 return array();
1104 }
1105
1106 /**
1107 * Upload image to WordPress media library
1108 *
1109 * @since 8.5.48
1110 */
1111 public function wpvr_upload_image() {
1112 if ( ! current_user_can( 'upload_files' ) ) {
1113 wp_send_json_error( array( 'message' => 'Unauthorized user' ), 403 );
1114 return;
1115 }
1116
1117 $nonce = isset($_POST['security']) ? sanitize_text_field(wp_unslash( $_POST['security'] )) : '';
1118 if ( !wp_verify_nonce( $nonce, 'wpvr' ) ) {
1119 wp_send_json_error( array( 'message' => 'Invalid nonce' ), 400 );
1120 return;
1121 }
1122
1123 if ( ! isset( $_FILES['image'] ) || empty( $_FILES['image']['tmp_name'] ) ) {
1124 wp_send_json_error( array( 'message' => 'No file uploaded' ) );
1125 return;
1126 }
1127
1128 $file_name = isset( $_FILES['image']['name'] ) ? sanitize_file_name( wp_unslash( $_FILES['image']['name'] ) ) : '';
1129 $file_type = wp_check_filetype( $file_name );
1130 $allowed_types = array( 'jpg', 'jpeg', 'png', 'webp' );
1131 if ( ! in_array( strtolower( $file_type['ext'] ), $allowed_types ) ) {
1132 wp_send_json_error( array( 'message' => 'Invalid file type. Only JPG, PNG, and WEBP are allowed.' ) );
1133 return;
1134 }
1135
1136 // Validate file size (max 50MB)
1137 $file_size = isset( $_FILES['image']['size'] ) ? absint( $_FILES['image']['size'] ) : 0;
1138 if ( $file_size > 50 * 1024 * 1024 ) {
1139 wp_send_json_error( array( 'message' => 'File size must be less than 50MB' ) );
1140 return;
1141 }
1142
1143 require_once( ABSPATH . 'wp-admin/includes/file.php' );
1144 require_once( ABSPATH . 'wp-admin/includes/media.php' );
1145 require_once( ABSPATH . 'wp-admin/includes/image.php' );
1146
1147 $upload = wp_handle_upload( $_FILES['image'], array( 'test_form' => false ) );
1148
1149 if ( isset( $upload['error'] ) ) {
1150 wp_send_json_error( array( 'message' => $upload['error'] ) );
1151 return;
1152 }
1153
1154 $attachment = array(
1155 'post_mime_type' => $upload['type'],
1156 'post_title' => sanitize_file_name( pathinfo( $file_name, PATHINFO_FILENAME ) ),
1157 'post_content' => '',
1158 'post_status' => 'inherit'
1159 );
1160
1161 $attach_id = wp_insert_attachment( $attachment, $upload['file'] );
1162 $attach_data = wp_generate_attachment_metadata( $attach_id, $upload['file'] );
1163 wp_update_attachment_metadata( $attach_id, $attach_data );
1164
1165 $image_url = wp_get_attachment_url( $attach_id );
1166
1167 wp_send_json_success( array(
1168 'attachment_id' => $attach_id,
1169 'url' => $image_url,
1170 'message' => 'Image uploaded successfully'
1171 ) );
1172 }
1173
1174 /**
1175 * Create tour from wizard data
1176 *
1177 * @since 8.5.48
1178 */
1179 public function wpvr_create_tour_from_wizard() {
1180 if ( ! current_user_can( 'edit_posts' ) ) {
1181 wp_send_json_error( array( 'message' => 'Unauthorized user' ), 403 );
1182 return;
1183 }
1184
1185 $nonce = isset($_POST['security']) ? sanitize_text_field(wp_unslash( $_POST['security'] )) : '';
1186 if ( !wp_verify_nonce( $nonce, 'wpvr' ) ) {
1187 wp_send_json_error( array( 'message' => 'Invalid nonce' ), 400 );
1188 return;
1189 }
1190
1191 $panodata = isset($_POST['panodata']) ? json_decode( wp_unslash( $_POST['panodata'] ), true ) : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1192 $title = isset($_POST['title']) ? sanitize_text_field(wp_unslash( $_POST['title'] )) : 'My Virtual Tour';
1193 $industry = isset($_POST['industry']) ? sanitize_text_field(wp_unslash( $_POST['industry'] )) : 'real-estate';
1194 $existing_post_id = isset($_POST['existing_post_id']) ? absint(wp_unslash( $_POST['existing_post_id'] )) : 0;
1195
1196 if ( empty( $panodata ) ) {
1197 wp_send_json_error( array( 'message' => 'Panodata is required' ) );
1198 return;
1199 }
1200
1201 if ( $existing_post_id > 0 ) {
1202 $existing_post = get_post( $existing_post_id );
1203 if ( ! $existing_post || 'wpvr_item' !== $existing_post->post_type || ! current_user_can( 'edit_post', $existing_post_id ) ) {
1204 wp_send_json_error( array( 'message' => 'Invalid existing tour ID' ) );
1205 return;
1206 }
1207
1208 $post_id = $existing_post_id;
1209 wp_update_post(
1210 array(
1211 'ID' => $post_id,
1212 'post_title' => $title,
1213 'post_status' => 'publish',
1214 )
1215 );
1216 } else {
1217 // Create new post
1218 $post_data = array(
1219 'post_title' => $title,
1220 'post_status' => 'publish',
1221 'post_type' => 'wpvr_item',
1222 'post_author' => get_current_user_id(),
1223 );
1224
1225 $post_id = wp_insert_post( $post_data );
1226
1227 if ( is_wp_error( $post_id ) ) {
1228 wp_send_json_error( array( 'message' => 'Failed to create tour: ' . $post_id->get_error_message() ) );
1229 return;
1230 }
1231 }
1232
1233 // Enforce local media URLs before final save/update
1234 $panodata = $this->wpvr_import_scene_attachments_to_media( $panodata, $post_id );
1235
1236 // Set panoid as pano{post_id} in panodata
1237 $panodata['panoid'] = 'pano' . $post_id;
1238
1239 // Normalize autoLoad to boolean so Pannellum's strict === true check passes.
1240 if ( isset( $panodata['autoLoad'] ) ) {
1241 $panodata['autoLoad'] = (bool) $panodata['autoLoad'];
1242 }
1243
1244 // Save panodata as post meta
1245 update_post_meta( $post_id, 'panodata', $panodata );
1246
1247 // Mark as created from wizard
1248 update_post_meta( $post_id, 'wpvr_created_from_wizard', true );
1249 update_post_meta( $post_id, 'wpvr_wizard_industry', $industry );
1250
1251 // Save template meta fields if provided (dynamic meta from API)
1252 $template_meta = isset($_POST['templateMeta']) ? json_decode( wp_unslash( $_POST['templateMeta'] ), true ) : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1253 if ( ! empty( $template_meta ) && is_array( $template_meta ) ) {
1254 foreach ( $template_meta as $meta_key => $meta_value ) {
1255 // Sanitize meta key to ensure it's a valid meta key
1256 $sanitized_key = sanitize_key( $meta_key );
1257 if ( ! empty( $sanitized_key ) && 'panodata' !== $sanitized_key ) {
1258 // Handle different value types
1259 if ( is_array( $meta_value ) ) {
1260 update_post_meta( $post_id, $sanitized_key, $meta_value );
1261 } else {
1262 update_post_meta( $post_id, $sanitized_key, sanitize_text_field( $meta_value ) );
1263 }
1264 }
1265 }
1266 }
1267
1268 // Trigger tour saved action for telemetry
1269 do_action('wpvr_rex_wpvr_tour_saved', $post_id);
1270 do_action( 'wpvr_setup_wizard_completed_event', $industry );
1271
1272 // Persist industry selection for telemetry (aha event fires later from consent handler).
1273 update_option( 'wpvr_industry_name', sanitize_text_field( $industry ), false );
1274
1275 // Mark wizard as permanently done so the onboarding notice is suppressed.
1276 update_option( 'wpvr_wizard_onboarding_done', '1', false );
1277
1278 wp_send_json_success( array(
1279 'post_id' => $post_id,
1280 'edit_url' => admin_url( 'post.php?action=edit&post=' . $post_id ),
1281 'view_url' => get_permalink( $post_id ),
1282 'message' => 'Tour created successfully'
1283 ) );
1284 }
1285
1286 }
1287