PluginProbe
WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress / 9.0.2
WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress v9.0.2
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.2, at legacy/admin/classes/class-wpvr-ajax.php

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