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

1,351 lines 48.4 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_setup_wizard' ) && ! 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 $post_type_obj = get_post_type_object( 'wpvr_item' );
469 $edit_cap = $post_type_obj ? $post_type_obj->cap->edit_posts : 'edit_wpvr_tours';
470 if ( ! current_user_can( $edit_cap ) || ! current_user_can( 'upload_files' ) ) {
471 $response = array(
472 'success' => false,
473 'data' => 'Permission denied.'
474 );
475 wp_send_json($response);
476 }
477 //===Current user capabilities check===//
478 //===Nonce check===//
479 $nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
480 if ( ! wp_verify_nonce( $nonce, 'wpvr' ) ) {
481 $response = array(
482 'success' => false,
483 'data' => 'Permission denied.'
484 );
485 wp_send_json($response);
486 }
487 $file_name = '';
488
489 if (
490 isset( $_FILES['wpvr_import_file'] )
491 && $_FILES['wpvr_import_file']['error'] === UPLOAD_ERR_OK
492 && ! empty( $_FILES['wpvr_import_file']['tmp_name'] )
493 ) {
494 $file = $_FILES['wpvr_import_file'];
495
496 // Validate file type - check if it's a ZIP file
497 $file_type = wp_check_filetype($file['name']);
498 $file_ext = strtolower($file_type['ext']);
499 if ($file_ext !== 'zip') {
500 wp_send_json_error(array('message' => 'Invalid file format. Only ZIP files are allowed.'));
501 return;
502 }
503
504 // Define a temporary filter to change the upload directory to our temp folder
505 $upload_dir_filter = function( $upload ) {
506 $upload['subdir'] = '/wpvr_imported_temp';
507 $upload['path'] = $upload['basedir'] . $upload['subdir'];
508 $upload['url'] = $upload['baseurl'] . $upload['subdir'];
509 return $upload;
510 };
511
512 // Apply the filter
513 add_filter( 'upload_dir', $upload_dir_filter );
514
515 // Use wp_handle_upload to securely handle the uploaded file
516 $upload_overrides = array( 'test_form' => false );
517 $movefile = wp_handle_upload( $file, $upload_overrides );
518
519 // Remove the filter immediately after upload
520 remove_filter( 'upload_dir', $upload_dir_filter );
521
522 if ( $movefile && ! isset( $movefile['error'] ) ) {
523 // Use the sanitized file name generated by WordPress
524 $file_name = basename( $movefile['file'] );
525 } else {
526 wp_send_json_error( array( 'message' => $movefile['error'] ) );
527 return;
528 }
529
530 } else {
531 $upload_error = isset( $_FILES['wpvr_import_file']['error'] ) ? (int) $_FILES['wpvr_import_file']['error'] : -1;
532 $error_messages = array(
533 UPLOAD_ERR_INI_SIZE => 'File exceeds the server upload size limit (upload_max_filesize).',
534 UPLOAD_ERR_FORM_SIZE => 'File exceeds the form upload size limit.',
535 UPLOAD_ERR_PARTIAL => 'File was only partially uploaded.',
536 UPLOAD_ERR_NO_FILE => 'No file selected.',
537 UPLOAD_ERR_NO_TMP_DIR => 'Missing temporary folder.',
538 UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk.',
539 UPLOAD_ERR_EXTENSION => 'A PHP extension stopped the upload.',
540 );
541 $message = isset( $error_messages[ $upload_error ] ) ? $error_messages[ $upload_error ] : 'No file selected.';
542 wp_send_json_error( array( 'message' => $message ) );
543 return;
544 }
545
546 //===Nonce check===//
547 WPVR_Import::prepare_tour_import_feature($file_name);
548 }
549
550
551
552 /**
553 * WPVR Role Management
554 *
555 * @return void
556 * @since 8.0.0
557 */
558 function wpvr_role_management()
559 {
560
561 //===Current user capabilities check===//
562 if (!current_user_can('manage_options')) {
563 $response = array(
564 'success' => false,
565 'data' => 'Permission denied.'
566 );
567 wp_send_json($response);
568 }
569 //===Current user capabilities check===//
570 //===Nonce check===//
571 $nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
572 if ( ! wp_verify_nonce( $nonce, 'wpvr' ) ) {
573 $response = array(
574 'success' => false,
575 'data' => 'Permission denied.'
576 );
577 wp_send_json($response);
578 }
579 //===Nonce check===//
580
581 $editor = isset( $_POST['editor'] ) ? sanitize_text_field( wp_unslash( $_POST['editor'] ) ) : '';
582 $author = isset( $_POST['author'] ) ? sanitize_text_field( wp_unslash( $_POST['author'] ) ) : '';
583 $fontawesome = isset( $_POST['fontawesome'] ) ? sanitize_text_field( wp_unslash( $_POST['fontawesome'] ) ) : '';
584
585 $cardboard = !empty($_POST['wpvr_cardboard_disable']) ? sanitize_text_field(wp_unslash( $_POST['wpvr_cardboard_disable'] )) : 'no'; //
586
587 $wpvr_webp_conversion = !empty($_POST['wpvr_webp_conversion']) ? sanitize_text_field(wp_unslash( $_POST['wpvr_webp_conversion'] )) : 'no';
588
589 $mobile_media_resize = isset( $_POST['mobile_media_resize'] ) ? sanitize_text_field( wp_unslash( $_POST['mobile_media_resize'] ) ) : '';
590 $high_res_image = isset( $_POST['high_res_image'] ) ? sanitize_text_field( wp_unslash( $_POST['high_res_image'] ) ) : '';
591 $dis_on_hover = isset( $_POST['dis_on_hover'] ) ? sanitize_text_field( wp_unslash( $_POST['dis_on_hover'] ) ) : '';
592 $wpvr_mobile_hotspot_tip = isset( $_POST['wpvr_mobile_hotspot_tip'] ) ? sanitize_text_field( wp_unslash( $_POST['wpvr_mobile_hotspot_tip'] ) ) : '';
593 $wpvr_frontend_notice = isset( $_POST['wpvr_frontend_notice'] ) ? sanitize_text_field( wp_unslash( $_POST['wpvr_frontend_notice'] ) ) : '';
594 $wpvr_frontend_notice_area = isset( $_POST['wpvr_frontend_notice_area'] ) ? sanitize_text_field( wp_unslash( $_POST['wpvr_frontend_notice_area'] ) ) : '';
595 $wpvr_script_control = isset( $_POST['wpvr_script_control'] ) ? sanitize_text_field( wp_unslash( $_POST['wpvr_script_control'] ) ) : '';
596 $wpvr_script_list = isset( $_POST['wpvr_script_list'] ) ? sanitize_text_field( wp_unslash( $_POST['wpvr_script_list'] ) ) : '';
597
598 $wpvr_video_script_control = isset( $_POST['wpvr_video_script_control'] ) ? sanitize_text_field( wp_unslash( $_POST['wpvr_video_script_control'] ) ) : '';
599 $wpvr_video_script_list = isset( $_POST['wpvr_video_script_list'] ) ? sanitize_text_field( wp_unslash( $_POST['wpvr_video_script_list'] ) ) : '';
600
601 // $enable_woocommerce = sanitize_text_field(wp_unslash( $_POST['woocommerce'] ));
602
603 $wpvr_script_list = str_replace(' ', '', $wpvr_script_list);
604
605 update_option('wpvr_editor_active', $editor);
606 update_option('wpvr_author_active', $author);
607 update_option('wpvr_fontawesome_disable', $fontawesome);
608 update_option('wpvr_cardboard_disable', $cardboard);
609 update_option('wpvr_webp_conversion', $wpvr_webp_conversion);
610 update_option('mobile_media_resize', $mobile_media_resize);
611 update_option('high_res_image', $high_res_image);
612 update_option('dis_on_hover', $dis_on_hover);
613 update_option('wpvr_mobile_hotspot_tip', 'true' === $wpvr_mobile_hotspot_tip ? 'true' : 'false');
614 update_option('wpvr_frontend_notice', $wpvr_frontend_notice);
615 update_option('wpvr_frontend_notice_area', $wpvr_frontend_notice_area);
616 update_option('wpvr_script_control', $wpvr_script_control);
617 update_option('wpvr_script_list', $wpvr_script_list);
618
619 update_option('wpvr_video_script_control', $wpvr_video_script_control);
620 update_option('wpvr_video_script_list', $wpvr_video_script_list);
621
622 if(is_plugin_active( 'dokan-lite/dokan.php' ) || is_plugin_active( 'dokan-pro/dokan.php' )){
623 $dokan_vendor = isset( $_POST['dokan_vendor'] ) ? sanitize_text_field(wp_unslash( $_POST['dokan_vendor'] )) : false;
624 update_option('dokan_vendor_active', $dokan_vendor);
625 }
626
627 // Usage data sharing toggle — sync with Linno telemetry SDK.
628 if ( isset( $_POST['wpvr_usage_tracking'] ) ) {
629 $tracking_toggle = sanitize_text_field( wp_unslash( $_POST['wpvr_usage_tracking'] ) );
630 $consent_state = 'true' === $tracking_toggle ? 'yes' : 'no';
631 $opt_in_numeric = 'yes' === $consent_state ? '1' : '0';
632
633 update_option( 'wpvr_allow_tracking', $consent_state );
634 update_option( 'wpvr_opt_in_toggle', $opt_in_numeric );
635
636 if ( function_exists( 'linno_telemetry' ) && defined( 'WPVR_FILE' ) ) {
637 $telemetry_client = linno_telemetry( WPVR_FILE );
638 if ( $telemetry_client && method_exists( $telemetry_client, 'set_optin_state' ) ) {
639 $telemetry_client->set_optin_state( $consent_state );
640 } elseif ( function_exists( 'linno_telemetry_sync_consent_state' ) ) {
641 linno_telemetry_sync_consent_state( WPVR_FILE );
642 }
643 }
644
645 if ( 'yes' === $consent_state ) {
646 do_action( 'wpvr_telemetry_consent_granted' );
647 }
648 }
649
650 // update_option('wpvr_enable_woocommerce', $enable_woocommerce);
651
652 $response = array(
653 'status' => 'success',
654 'message' => 'Successfully saved',
655 );
656 wp_send_json($response);
657 }
658
659
660 /**
661 * WPVR Notice
662 *
663 * @return void
664 * @since 8.0.0
665 */
666 function wpvr_notice()
667 {
668 //===Current user capabilities check===//
669 if (!current_user_can('manage_options')) {
670 $response = array(
671 'success' => false,
672 'data' => 'Permission denied.'
673 );
674 wp_send_json($response);
675 }
676 //===Current user capabilities check===//
677 //===Nonce check===//
678 $nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
679 if ( ! wp_verify_nonce( $nonce, 'wpvr' ) ) {
680 $response = array(
681 'success' => false,
682 'data' => 'Permission denied.'
683 );
684 wp_send_json($response);
685 }
686 //===Nonce check===//
687 update_option('wpvr_black_friday_notice', '1');
688 }
689
690 /**
691 * Dismiss black friday notice
692 */
693 function dismiss_black_friday_notice(){
694 if( !current_user_can( 'manage_options' ) ){
695 wp_send_json_error( array( 'message' => 'Unauthorized user' ), 403 );
696 return;
697 }
698 if (!isset($_POST['nonce']) || !wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'wpvr')) {
699 wp_die(esc_html__('Permission check failed', 'wpvr'));
700 }
701 update_option('_wpvr_eid_al_adha_2024', 'yes');
702 echo json_encode(['success' => true,]);
703 wp_die();
704 }
705
706 /**
707 * Handles the creation of a contact via a webhook.
708 *
709 * This function validates the nonce, sanitizes and validates the input fields,
710 * and then creates a new contact using the WPVR_Create_Contact class.
711 *
712 * @since 8.4.10
713 */
714 function wpvr_create_contact(){
715 if( !current_user_can( 'manage_options' ) ){
716 wp_send_json_error( array( 'message' => 'Unauthorized user' ), 403 );
717 return;
718 }
719 $nonce = filter_input(INPUT_POST, 'security', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
720 $nonce = !empty( $nonce ) ? $nonce : null;
721 if ( ! wp_verify_nonce( $nonce, 'wpvr_setup_wizard' ) && ! wp_verify_nonce( $nonce, 'wpvr' ) ) {
722 wp_send_json_error( array( 'message' => 'Invalid nonce' ), 400 );
723 return;
724 }
725
726 $name = filter_input(INPUT_POST, 'name', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
727 $industry = filter_input(INPUT_POST, 'industry', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
728 $email = filter_input(INPUT_POST, 'email', FILTER_SANITIZE_EMAIL);
729 $opt_in = filter_input(INPUT_POST, 'opt_in', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
730
731 $name = !empty($name) ? $name: '';
732 $industry = !empty($industry ) ? $industry : '';
733 $email = !empty( $email ) ? $email : '';
734
735 if ( empty( $email ) ) {
736 wp_send_json_error( array( 'message' => __('Email is required', 'wpvr') ), 400 );
737 }elseif( !is_email( $email ) ){
738 wp_send_json_error( array( 'message' => __('Email is invalid', 'wpvr') ), 400 );
739 }
740
741 $create_contact_instance = new WPVR_Create_Contact( $email, $name, $industry );
742 $response = $create_contact_instance->create_contact_via_webhook();
743
744 update_option('wpvr_posthog_access_enabled', $opt_in);
745
746
747 if ( $response ) {
748 wp_send_json_success( array( 'message' => __('Contact created successfully', 'wpvr') ), 200 );
749 } else {
750 wp_send_json_error( array( 'message' => __('Failed to create contact', 'wpvr') ), 500 );
751 }
752 }
753
754 /**
755 * Saves the general settings for the WPVR plugin.
756 *
757 * This function handles the nonce verification, sanitizes the input fields,
758 * and updates the options in the database. It responds with a JSON success or error message.
759 *
760 * @since 8.4.10
761 */
762 function wpvr_save_general_settings(){
763
764 if ( ! current_user_can( 'manage_options' ) ) {
765 wp_send_json_error( array( 'message' => 'Unauthorized user' ), 403 );
766 return;
767 }
768
769 $nonce = filter_input(INPUT_POST, 'security', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
770 $nonce = !empty( $nonce ) ? $nonce : null; // phpcs:ignore
771 if ( !wp_verify_nonce( $nonce, 'wpvr' ) ) {
772 wp_send_json_error( array( 'message' => 'Invalid nonce' ), 400 );
773 return;
774 }
775
776 $is_mobile_media_resize = filter_input(INPUT_POST, 'media_resizer', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
777 $convert_to_webp = filter_input(INPUT_POST, 'convert_to_webp', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
778 $vr_glass_support = filter_input(INPUT_POST, 'vr_glass_support', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
779
780 update_option('mobile_media_resize', $is_mobile_media_resize);
781 update_option('wpvr_webp_conversion', $convert_to_webp);
782 update_option('wpvr_cardboard_disable', $vr_glass_support);
783
784 wp_send_json_success( array( 'message' => __('General setting data successfully saved.', 'wpvr') ), 200 );
785 }
786
787
788 /**
789 * AJAX handler to persist opt-in toggle value
790 *
791 */
792 public function wpvr_save_opt_in_toggle() {
793 if ( ! current_user_can( 'manage_options' ) ) {
794 wp_send_json_error( array( 'message' => 'Unauthorized user' ), 403 );
795 return;
796 }
797
798 $nonce = isset($_POST['security']) ? sanitize_text_field(wp_unslash( $_POST['security'] )) : '';
799 if ( ! wp_verify_nonce( $nonce, 'wpvr_setup_wizard' ) && ! wp_verify_nonce( $nonce, 'wpvr' ) ) {
800 wp_send_json_error( array( 'message' => 'Invalid nonce' ), 400 );
801 return;
802 }
803
804 $opt_in = isset($_POST['opt_in']) ? sanitize_text_field(wp_unslash( $_POST['opt_in'] )) : '0';
805 $consent_state = '1' === $opt_in ? 'yes' : 'no';
806
807 update_option('wpvr_opt_in_toggle', $opt_in);
808 update_option('wpvr_allow_tracking', $consent_state);
809
810 if ( 'yes' === $consent_state ) {
811 $this->wpvr_create_contact_for_current_user();
812 }
813
814 if ( function_exists( 'linno_telemetry' ) && defined( 'WPVR_FILE' ) ) {
815 $telemetry_client = linno_telemetry( WPVR_FILE );
816
817 if ( $telemetry_client && method_exists( $telemetry_client, 'set_optin_state' ) ) {
818 $telemetry_client->set_optin_state( $consent_state );
819 } elseif ( function_exists( 'linno_telemetry_sync_consent_state' ) ) {
820 linno_telemetry_sync_consent_state( WPVR_FILE );
821 }
822 }
823
824 // Fire after SDK consent is fully synced so consent-gated events can queue.
825 if ( 'yes' === $consent_state ) {
826 do_action( 'wpvr_telemetry_consent_granted' );
827 }
828
829 wp_send_json_success( array( 'message' => __('Opt-in value saved.', 'wpvr') ), 200 );
830 }
831
832
833 /**
834 * Create webhook contact from current user after consent.
835 *
836 * @return void
837 */
838 private function wpvr_create_contact_for_current_user() {
839 $current_user = wp_get_current_user();
840 if ( ! $current_user || empty( $current_user->user_email ) ) {
841 return;
842 }
843
844 $email = sanitize_email( $current_user->user_email );
845 if ( ! is_email( $email ) ) {
846 return;
847 }
848
849 $name = sanitize_text_field( $current_user->display_name );
850 if ( empty( $name ) ) {
851 $name = sanitize_text_field( $current_user->user_login );
852 }
853
854 $industry = sanitize_text_field( get_option( 'wpvr_industry_name', '' ) );
855
856 $create_contact_instance = new WPVR_Create_Contact( $email, $name, $industry );
857 $create_contact_instance->create_contact_via_webhook();
858 }
859
860
861 /**
862 * Fetch template tour object from remote API
863 *
864 * @since 8.5.48
865 */
866 public function wpvr_fetch_template() {
867 if ( ! current_user_can( 'manage_options' ) ) {
868 wp_send_json_error( array( 'message' => 'Unauthorized user' ), 403 );
869 return;
870 }
871
872 $nonce = isset($_POST['security']) ? sanitize_text_field(wp_unslash( $_POST['security'] )) : '';
873 if ( ! wp_verify_nonce( $nonce, 'wpvr_setup_wizard' ) && ! wp_verify_nonce( $nonce, 'wpvr' ) ) {
874 wp_send_json_error( array( 'message' => 'Invalid nonce' ), 400 );
875 return;
876 }
877
878 $industry = isset($_POST['industry']) ? sanitize_text_field(wp_unslash( $_POST['industry'] )) : 'real-estate';
879
880 // Static industry to remote tour ID mapping
881 $industry_id_map = array(
882 'exhibitions' => 2140,
883 'offices' => 2145,
884 'real-estate' => 2147,
885 'hotel' => 2149,
886 'ecommerce' => 2151,
887 'showrooms' => 2153,
888 'school' => 2155,
889 );
890
891 // Get source tour ID for the selected industry
892 $source_tour_id = isset($industry_id_map[$industry]) ? $industry_id_map[$industry] : 2147;
893
894 // Build API URL with source tour ID
895 $api_url = 'https://showcase.rextheme.com/wp-json/wpvr/v1/tour/' . intval($source_tour_id);
896 $api_url = apply_filters('wpvr_template_api_url', $api_url, $industry, $source_tour_id);
897 $response = wp_remote_get($api_url, array(
898 'timeout' => 30,
899 'headers' => array(
900 'Content-Type' => 'application/json',
901 'Accept' => 'application/json',
902 ),
903 ));
904
905 if ( is_wp_error( $response ) ) {
906 wp_send_json_error( array( 'message' => 'Failed to fetch template: ' . $response->get_error_message() ) );
907 return;
908 }
909
910 $status_code = wp_remote_retrieve_response_code( $response );
911 if ( $status_code !== 200 ) {
912 wp_send_json_error( array( 'message' => 'Template not found (HTTP ' . $status_code . ')' ) );
913 return;
914 }
915
916 $body = wp_remote_retrieve_body( $response );
917 $api_data = json_decode( $body, true );
918
919 if ( ! $api_data || ! is_array( $api_data ) ) {
920 wp_send_json_error( array( 'message' => 'Invalid template data received' ) );
921 return;
922 }
923
924 $remote_meta = array();
925 if ( isset( $api_data['meta_data'] ) && is_array( $api_data['meta_data'] ) ) {
926 $remote_meta = $api_data['meta_data'];
927 } elseif ( isset( $api_data['meta'] ) && is_array( $api_data['meta'] ) ) {
928 $remote_meta = $api_data['meta'];
929 }
930
931 $panodata = array();
932 if ( isset( $remote_meta['panodata'] ) ) {
933 $panodata = $this->wpvr_normalize_panodata( $remote_meta['panodata'] );
934 }
935 if ( empty( $panodata ) && isset( $api_data['panodata'] ) ) {
936 $panodata = $this->wpvr_normalize_panodata( $api_data['panodata'] );
937 }
938
939 if ( empty( $panodata ) ) {
940 wp_send_json_error( array( 'message' => 'Template panodata missing in API response' ) );
941 return;
942 }
943
944 $title = isset( $api_data['title'] ) && ! empty( $api_data['title'] )
945 ? sanitize_text_field( $api_data['title'] )
946 : 'My Virtual Tour';
947
948 $post_data = array(
949 'post_title' => $title,
950 'post_status' => 'publish',
951 'post_type' => 'wpvr_item',
952 'post_author' => get_current_user_id(),
953 // meta_input is written before wp_insert_post() fires transition_post_status,
954 // so telemetry listening on that hook sees these flags already set.
955 'meta_input' => array(
956 'wpvr_created_from_wizard' => true,
957 'wpvr_wizard_industry' => $industry,
958 ),
959 );
960
961 $post_id = wp_insert_post( $post_data );
962 if ( is_wp_error( $post_id ) ) {
963 wp_send_json_error( array( 'message' => 'Failed to create tour: ' . $post_id->get_error_message() ) );
964 return;
965 }
966
967 $panodata = $this->wpvr_import_scene_attachments_to_media( $panodata, $post_id );
968 $panodata['panoid'] = 'pano' . $post_id;
969
970 // Keep meta panodata in sync with imported local scene URLs
971 if ( ! is_array( $remote_meta ) ) {
972 $remote_meta = array();
973 }
974 $remote_meta['panodata'] = $panodata;
975
976 update_post_meta( $post_id, 'panodata', $panodata );
977
978 if ( ! empty( $remote_meta ) ) {
979 foreach ( $remote_meta as $meta_key => $meta_value ) {
980 $sanitized_key = sanitize_key( $meta_key );
981 if ( empty( $sanitized_key ) || 'panodata' === $sanitized_key ) {
982 continue;
983 }
984
985 if ( is_array( $meta_value ) ) {
986 update_post_meta( $post_id, $sanitized_key, $meta_value );
987 } else {
988 update_post_meta( $post_id, $sanitized_key, sanitize_text_field( $meta_value ) );
989 }
990 }
991 }
992
993 $template_data = array(
994 'industry' => $industry,
995 'template_id' => $source_tour_id,
996 'post_id' => $post_id,
997 'edit_url' => admin_url( 'post.php?action=edit&post=' . $post_id ),
998 'view_url' => get_permalink( $post_id ),
999 'panodata' => $panodata,
1000 'meta' => $remote_meta,
1001 );
1002
1003 if ( isset( $panodata['panodata']['scene-list']['1']['scene-attachment-url'] ) ) {
1004 $template_data['image_url'] = esc_url_raw( $panodata['panodata']['scene-list']['1']['scene-attachment-url'] );
1005 } elseif ( isset( $api_data['image_url'] ) ) {
1006 $template_data['image_url'] = esc_url_raw( $api_data['image_url'] );
1007 } elseif ( isset( $api_data['featured_image'] ) ) {
1008 $template_data['image_url'] = esc_url_raw( $api_data['featured_image'] );
1009 }
1010
1011 do_action('wpvr_rex_wpvr_tour_saved', $post_id);
1012
1013 wp_send_json_success( array( 'template' => $template_data ) );
1014 }
1015
1016 /**
1017 * Import scene attachment URLs into media library and replace URLs in panodata.
1018 *
1019 * @param array $panodata Panodata structure.
1020 * @param int $post_id Target post ID.
1021 *
1022 * @return array
1023 */
1024 private function wpvr_import_scene_attachments_to_media( $panodata, $post_id ) {
1025 if ( empty( $panodata['panodata']['scene-list'] ) || ! is_array( $panodata['panodata']['scene-list'] ) ) {
1026 return $panodata;
1027 }
1028
1029 require_once( ABSPATH . 'wp-admin/includes/file.php' );
1030 require_once( ABSPATH . 'wp-admin/includes/media.php' );
1031 require_once( ABSPATH . 'wp-admin/includes/image.php' );
1032
1033 $scene_image_keys = array(
1034 'scene-attachment-url',
1035 'scene-attachment-url-face0',
1036 'scene-attachment-url-face1',
1037 'scene-attachment-url-face2',
1038 'scene-attachment-url-face3',
1039 'scene-attachment-url-face4',
1040 'scene-attachment-url-face5',
1041 );
1042
1043 $imported_urls = array();
1044
1045 foreach ( $panodata['panodata']['scene-list'] as $scene_key => $scene ) {
1046 if ( ! is_array( $scene ) ) {
1047 continue;
1048 }
1049
1050 foreach ( $scene_image_keys as $image_key ) {
1051 if ( empty( $scene[ $image_key ] ) || ! is_string( $scene[ $image_key ] ) ) {
1052 continue;
1053 }
1054
1055 $source_url = esc_url_raw( $scene[ $image_key ] );
1056 if ( empty( $source_url ) ) {
1057 continue;
1058 }
1059
1060 if ( isset( $imported_urls[ $source_url ] ) ) {
1061 $panodata['panodata']['scene-list'][ $scene_key ][ $image_key ] = $imported_urls[ $source_url ];
1062 continue;
1063 }
1064
1065 $attachment_id = attachment_url_to_postid( $source_url );
1066 if ( ! $attachment_id ) {
1067 if ( ! current_user_can( 'upload_files' ) ) {
1068 continue;
1069 }
1070
1071 $valid_url = wp_http_validate_url( $source_url );
1072 if ( ! $valid_url ) {
1073 continue;
1074 }
1075
1076 $attachment_id = media_sideload_image( $valid_url, $post_id, null, 'id' );
1077 if ( is_wp_error( $attachment_id ) ) {
1078 continue;
1079 }
1080 }
1081
1082 $local_url = wp_get_attachment_url( $attachment_id );
1083 if ( ! empty( $local_url ) ) {
1084 $local_url = esc_url_raw( $local_url );
1085 $imported_urls[ $source_url ] = $local_url;
1086 $panodata['panodata']['scene-list'][ $scene_key ][ $image_key ] = $local_url;
1087 }
1088 }
1089 }
1090
1091 return $panodata;
1092 }
1093
1094 /**
1095 * Normalize panodata payloads from array/serialized/json values.
1096 *
1097 * @param mixed $raw_panodata Panodata from remote API/meta.
1098 *
1099 * @return array
1100 */
1101 private function wpvr_normalize_panodata( $raw_panodata ) {
1102 if ( is_array( $raw_panodata ) ) {
1103 return $raw_panodata;
1104 }
1105
1106 if ( is_string( $raw_panodata ) && '' !== $raw_panodata ) {
1107 $unserialized = maybe_unserialize( $raw_panodata );
1108 if ( is_array( $unserialized ) ) {
1109 return $unserialized;
1110 }
1111
1112 $decoded_json = json_decode( $raw_panodata, true );
1113 if ( is_array( $decoded_json ) ) {
1114 return $decoded_json;
1115 }
1116 }
1117
1118 return array();
1119 }
1120
1121 /**
1122 * Upload image to WordPress media library
1123 *
1124 * @since 8.5.48
1125 */
1126 public function wpvr_upload_image() {
1127 if ( ! current_user_can( 'upload_files' ) ) {
1128 wp_send_json_error( array( 'message' => 'Unauthorized user' ), 403 );
1129 return;
1130 }
1131
1132 $nonce = isset($_POST['security']) ? sanitize_text_field(wp_unslash( $_POST['security'] )) : '';
1133 if ( ! wp_verify_nonce( $nonce, 'wpvr_setup_wizard' ) && ! wp_verify_nonce( $nonce, 'wpvr' ) ) {
1134 wp_send_json_error( array( 'message' => 'Invalid nonce' ), 400 );
1135 return;
1136 }
1137
1138 if ( ! isset( $_FILES['image'] ) || empty( $_FILES['image']['tmp_name'] ) ) {
1139 wp_send_json_error( array( 'message' => 'No file uploaded' ) );
1140 return;
1141 }
1142
1143 $file_name = isset( $_FILES['image']['name'] ) ? sanitize_file_name( wp_unslash( $_FILES['image']['name'] ) ) : '';
1144 $file_type = wp_check_filetype( $file_name );
1145 $allowed_types = array( 'jpg', 'jpeg', 'png', 'webp' );
1146 if ( ! in_array( strtolower( $file_type['ext'] ), $allowed_types ) ) {
1147 wp_send_json_error( array( 'message' => 'Invalid file type. Only JPG, PNG, and WEBP are allowed.' ) );
1148 return;
1149 }
1150
1151 // Validate file size (max 50MB)
1152 $file_size = isset( $_FILES['image']['size'] ) ? absint( $_FILES['image']['size'] ) : 0;
1153 if ( $file_size > 50 * 1024 * 1024 ) {
1154 wp_send_json_error( array( 'message' => 'File size must be less than 50MB' ) );
1155 return;
1156 }
1157
1158 require_once( ABSPATH . 'wp-admin/includes/file.php' );
1159 require_once( ABSPATH . 'wp-admin/includes/media.php' );
1160 require_once( ABSPATH . 'wp-admin/includes/image.php' );
1161
1162 $upload = wp_handle_upload( $_FILES['image'], array( 'test_form' => false ) );
1163
1164 if ( isset( $upload['error'] ) ) {
1165 wp_send_json_error( array( 'message' => $upload['error'] ) );
1166 return;
1167 }
1168
1169 $attachment = array(
1170 'post_mime_type' => $upload['type'],
1171 'post_title' => sanitize_file_name( pathinfo( $file_name, PATHINFO_FILENAME ) ),
1172 'post_content' => '',
1173 'post_status' => 'inherit'
1174 );
1175
1176 $attach_id = wp_insert_attachment( $attachment, $upload['file'] );
1177 $attach_data = wp_generate_attachment_metadata( $attach_id, $upload['file'] );
1178 wp_update_attachment_metadata( $attach_id, $attach_data );
1179
1180 $image_url = wp_get_attachment_url( $attach_id );
1181
1182 wp_send_json_success( array(
1183 'attachment_id' => $attach_id,
1184 'url' => $image_url,
1185 'message' => 'Image uploaded successfully'
1186 ) );
1187 }
1188
1189 /**
1190 * Create tour from wizard data
1191 *
1192 * @since 8.5.48
1193 */
1194 public function wpvr_create_tour_from_wizard() {
1195 $post_type_obj = get_post_type_object( 'wpvr_item' );
1196 $create_cap = $post_type_obj ? $post_type_obj->cap->edit_posts : 'edit_wpvr_tours';
1197 if ( ! current_user_can( $create_cap ) ) {
1198 wp_send_json_error( array( 'message' => 'Unauthorized user' ), 403 );
1199 return;
1200 }
1201
1202 $nonce = isset($_POST['security']) ? sanitize_text_field(wp_unslash( $_POST['security'] )) : '';
1203 if ( ! wp_verify_nonce( $nonce, 'wpvr_setup_wizard' ) && ! wp_verify_nonce( $nonce, 'wpvr' ) ) {
1204 wp_send_json_error( array( 'message' => 'Invalid nonce' ), 400 );
1205 return;
1206 }
1207
1208 $panodata = isset($_POST['panodata']) ? json_decode( wp_unslash( $_POST['panodata'] ), true ) : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1209 $title = isset($_POST['title']) ? sanitize_text_field(wp_unslash( $_POST['title'] )) : 'My Virtual Tour';
1210 $industry = isset($_POST['industry']) ? sanitize_text_field(wp_unslash( $_POST['industry'] )) : 'real-estate';
1211 $existing_post_id = isset($_POST['existing_post_id']) ? absint(wp_unslash( $_POST['existing_post_id'] )) : 0;
1212
1213 if ( empty( $panodata ) ) {
1214 wp_send_json_error( array( 'message' => 'Panodata is required' ) );
1215 return;
1216 }
1217
1218 if ( is_array( $panodata ) ) {
1219 if ( isset( $panodata['previewtext'] ) ) {
1220 $panodata['previewtext'] = sanitize_text_field( $panodata['previewtext'] );
1221 }
1222 if ( isset( $panodata['defaultscene'] ) && is_string( $panodata['defaultscene'] ) ) {
1223 $panodata['defaultscene'] = preg_replace( '/[^0-9a-zA-Z_\-]/', '', $panodata['defaultscene'] );
1224 }
1225 if ( isset( $panodata['panodata']['firstScene'] ) && is_string( $panodata['panodata']['firstScene'] ) ) {
1226 $panodata['panodata']['firstScene'] = preg_replace( '/[^0-9a-zA-Z_\-]/', '', $panodata['panodata']['firstScene'] );
1227 }
1228 if ( isset( $panodata['panodata']['scene-list'] ) && is_array( $panodata['panodata']['scene-list'] ) ) {
1229 foreach ( $panodata['panodata']['scene-list'] as $s_idx => $s_val ) {
1230 if ( isset( $s_val['scene-id'] ) && is_string( $s_val['scene-id'] ) ) {
1231 $panodata['panodata']['scene-list'][ $s_idx ]['scene-id'] = preg_replace( '/[^0-9a-zA-Z_\-]/', '', $s_val['scene-id'] );
1232 }
1233 }
1234 }
1235 }
1236
1237 $publish_cap = $post_type_obj ? $post_type_obj->cap->publish_posts : 'publish_wpvr_tours';
1238 $target_status = current_user_can( $publish_cap ) ? 'publish' : 'draft';
1239
1240 if ( $existing_post_id > 0 ) {
1241 $existing_post = get_post( $existing_post_id );
1242 if ( ! $existing_post || 'wpvr_item' !== $existing_post->post_type || ! current_user_can( 'edit_post', $existing_post_id ) ) {
1243 wp_send_json_error( array( 'message' => 'Invalid existing tour ID' ) );
1244 return;
1245 }
1246
1247 $post_id = $existing_post_id;
1248 wp_update_post(
1249 array(
1250 'ID' => $post_id,
1251 'post_title' => $title,
1252 'post_status' => $target_status,
1253 // See note below: meta_input lands before transition_post_status fires.
1254 'meta_input' => array(
1255 'wpvr_created_from_wizard' => true,
1256 'wpvr_wizard_industry' => $industry,
1257 ),
1258 )
1259 );
1260 } else {
1261 // Create new post
1262 $post_data = array(
1263 'post_title' => $title,
1264 'post_status' => $target_status,
1265 'post_type' => 'wpvr_item',
1266 'post_author' => get_current_user_id(),
1267 // meta_input is written before wp_insert_post() fires transition_post_status,
1268 // so telemetry listening on that hook sees these flags already set.
1269 'meta_input' => array(
1270 'wpvr_created_from_wizard' => true,
1271 'wpvr_wizard_industry' => $industry,
1272 ),
1273 );
1274
1275 $post_id = wp_insert_post( $post_data );
1276
1277 if ( is_wp_error( $post_id ) ) {
1278 wp_send_json_error( array( 'message' => 'Failed to create tour: ' . $post_id->get_error_message() ) );
1279 return;
1280 }
1281 }
1282
1283 // Enforce local media URLs before final save/update
1284 $panodata = $this->wpvr_import_scene_attachments_to_media( $panodata, $post_id );
1285
1286 // Set panoid as pano{post_id} in panodata
1287 $panodata['panoid'] = 'pano' . $post_id;
1288
1289 // Normalize autoLoad to boolean so Pannellum's strict === true check passes.
1290 if ( isset( $panodata['autoLoad'] ) ) {
1291 $panodata['autoLoad'] = (bool) $panodata['autoLoad'];
1292 }
1293
1294 // Save panodata as post meta
1295 update_post_meta( $post_id, 'panodata', $panodata );
1296
1297 // Save template meta fields if provided (dynamic meta from API)
1298 $template_meta = isset($_POST['templateMeta']) ? json_decode( wp_unslash( $_POST['templateMeta'] ), true ) : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1299 if ( ! empty( $template_meta ) && is_array( $template_meta ) ) {
1300 $allowed_meta_keys = apply_filters(
1301 'wpvr_wizard_allowed_template_meta_keys',
1302 array(
1303 'wpvr_created_from_wizard',
1304 'wpvr_wizard_industry',
1305 'wpvr_tour_layout',
1306 'wpvr_custom_css',
1307 'wpvr_streetview_settings',
1308 'wpvr_general_settings',
1309 'wpvr_controls_settings',
1310 'wpvr_floor_plan_settings',
1311 'wpvr_checklist',
1312 )
1313 );
1314
1315 foreach ( $template_meta as $meta_key => $meta_value ) {
1316 // Sanitize meta key to ensure it's a valid meta key
1317 $sanitized_key = sanitize_key( $meta_key );
1318 if ( ! empty( $sanitized_key ) && in_array( $sanitized_key, $allowed_meta_keys, true ) ) {
1319 // Handle different value types
1320 if ( is_array( $meta_value ) ) {
1321 $sanitized_value = map_deep( $meta_value, 'sanitize_text_field' );
1322 update_post_meta( $post_id, $sanitized_key, $sanitized_value );
1323 } else {
1324 update_post_meta( $post_id, $sanitized_key, sanitize_text_field( $meta_value ) );
1325 }
1326 }
1327 }
1328 }
1329
1330 // Trigger tour saved action for telemetry
1331 do_action('wpvr_rex_wpvr_tour_saved', $post_id);
1332 do_action( 'wpvr_setup_wizard_completed_event', $industry );
1333
1334 if ( current_user_can( 'manage_options' ) ) {
1335 // Persist industry selection for telemetry (aha event fires later from consent handler).
1336 update_option( 'wpvr_industry_name', sanitize_text_field( $industry ), false );
1337
1338 // Mark wizard as permanently done so the onboarding notice is suppressed.
1339 update_option( 'wpvr_wizard_onboarding_done', '1', false );
1340 }
1341
1342 wp_send_json_success( array(
1343 'post_id' => $post_id,
1344 'edit_url' => admin_url( 'post.php?action=edit&post=' . $post_id ),
1345 'view_url' => get_permalink( $post_id ),
1346 'message' => 'Tour created successfully'
1347 ) );
1348 }
1349
1350 }
1351