PluginProbe
WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress / 8.5.73
WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress v8.5.73
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.73, at admin/classes/class-wpvr-ajax.php

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