PluginProbe
WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress / 8.5.77
WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress v8.5.77
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 8.5.4 All 221 releases
wpvr / admin / classes / class-wpvr-ajax.php

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

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