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

1,219 lines 40.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if (!defined('ABSPATH')) exit; // Exit if accessed directly
4 /**
5 * The admin-specific Ajax files.
6 *
7 * @link http://rextheme.com/
8 * @since 8.0.0
9 *
10 * @package Wpvr
11 * @subpackage Wpvr/admin
12 */
13
14 class Wpvr_Ajax
15 {
16
17 /**
18 * Instance of WPVR_Format class
19 *
20 * @var object
21 * @since 8.0.0
22 */
23 protected $format;
24
25
26 /**
27 * Instance of WPVR_StreetView class
28 *
29 * @var object
30 * @since 8.0.0
31 */
32 protected $streetview;
33
34
35 /**
36 * Instance of WPVR_Video class
37 *
38 * @var object
39 * @since 8.0.0
40 */
41 protected $video;
42
43
44 /**
45 * Instance of WPVR_Scene class
46 *
47 * @var object
48 * @since 8.0.0
49 */
50 protected $scene;
51
52
53 /**
54 * Instance of WPVR_Validator class
55 *
56 * @var object
57 * @since 8.0.0
58 */
59 protected $validator;
60
61
62 function __construct()
63 {
64 $this->format = new WPVR_Format();
65 $this->streetview = new WPVR_StreetView();
66 $this->video = new WPVR_Video();
67 $this->scene = new WPVR_Scene();
68 $this->validator = new WPVR_Validator();
69
70 add_action('wp_ajax_wpvr_save', array($this, 'wpvr_save_data'));
71 add_action('wp_ajax_wpvr_preview', array($this, 'wpvr_show_preview'));
72 add_action('wp_ajax_wpvrstreetview_preview', array($this, 'wpvrstreetview_preview'));
73 add_action('wp_ajax_wpvr_file_import', array($this, 'wpvr_file_import'));
74 add_action('wp_ajax_wpvr_role_management', array($this, 'wpvr_role_management'));
75 add_action('wp_ajax_wpvr_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_frontend_notice = sanitize_text_field($_POST['wpvr_frontend_notice']);
538 $wpvr_frontend_notice_area = sanitize_text_field($_POST['wpvr_frontend_notice_area']);
539 $wpvr_script_control = sanitize_text_field($_POST['wpvr_script_control']);
540 $wpvr_script_list = sanitize_text_field($_POST['wpvr_script_list']);
541
542 $wpvr_video_script_control = sanitize_text_field($_POST['wpvr_video_script_control']);
543 $wpvr_video_script_list = sanitize_text_field($_POST['wpvr_video_script_list']);
544
545 // $enable_woocommerce = sanitize_text_field($_POST['woocommerce']);
546
547 $wpvr_script_list = str_replace(' ', '', $wpvr_script_list);
548
549 update_option('wpvr_editor_active', $editor);
550 update_option('wpvr_author_active', $author);
551 update_option('wpvr_fontawesome_disable', $fontawesome);
552 update_option('wpvr_cardboard_disable', $cardboard);
553 update_option('wpvr_webp_conversion', $wpvr_webp_conversion);
554 update_option('mobile_media_resize', $mobile_media_resize);
555 update_option('high_res_image', $high_res_image);
556 update_option('dis_on_hover', $dis_on_hover);
557 update_option('wpvr_frontend_notice', $wpvr_frontend_notice);
558 update_option('wpvr_frontend_notice_area', $wpvr_frontend_notice_area);
559 update_option('wpvr_script_control', $wpvr_script_control);
560 update_option('wpvr_script_list', $wpvr_script_list);
561
562 update_option('wpvr_video_script_control', $wpvr_video_script_control);
563 update_option('wpvr_video_script_list', $wpvr_video_script_list);
564
565 if(is_plugin_active( 'dokan-lite/dokan.php' ) || is_plugin_active( 'dokan-pro/dokan.php' )){
566 $dokan_vendor = isset( $_POST['dokan_vendor'] ) ? sanitize_text_field($_POST['dokan_vendor']) : false;
567 update_option('dokan_vendor_active', $dokan_vendor);
568 }
569
570 // Usage data sharing toggle — sync with Linno telemetry SDK.
571 if ( isset( $_POST['wpvr_usage_tracking'] ) ) {
572 $tracking_toggle = sanitize_text_field( $_POST['wpvr_usage_tracking'] );
573 $consent_state = 'true' === $tracking_toggle ? 'yes' : 'no';
574 $opt_in_numeric = 'yes' === $consent_state ? '1' : '0';
575
576 update_option( 'wpvr_allow_tracking', $consent_state );
577 update_option( 'wpvr_opt_in_toggle', $opt_in_numeric );
578
579 if ( function_exists( 'linno_telemetry' ) && defined( 'WPVR_FILE' ) ) {
580 $telemetry_client = linno_telemetry( WPVR_FILE );
581 if ( $telemetry_client && method_exists( $telemetry_client, 'set_optin_state' ) ) {
582 $telemetry_client->set_optin_state( $consent_state );
583 } elseif ( function_exists( 'linno_telemetry_sync_consent_state' ) ) {
584 linno_telemetry_sync_consent_state( WPVR_FILE );
585 }
586 }
587
588 if ( 'yes' === $consent_state ) {
589 do_action( 'wpvr_telemetry_consent_granted' );
590 }
591 }
592
593 // update_option('wpvr_enable_woocommerce', $enable_woocommerce);
594
595 $response = array(
596 'status' => 'success',
597 'message' => 'Successfully saved',
598 );
599 wp_send_json($response);
600 }
601
602
603 /**
604 * WPVR Notice
605 *
606 * @return void
607 * @since 8.0.0
608 */
609 function wpvr_notice()
610 {
611 //===Current user capabilities check===//
612 if (!current_user_can('manage_options')) {
613 $response = array(
614 'success' => false,
615 'data' => 'Permission denied.'
616 );
617 wp_send_json($response);
618 }
619 //===Current user capabilities check===//
620 //===Nonce check===//
621 $nonce = sanitize_text_field($_POST['nonce']);
622 if (!wp_verify_nonce($nonce, 'wpvr')) {
623 $response = array(
624 'success' => false,
625 'data' => 'Permission denied.'
626 );
627 wp_send_json($response);
628 }
629 //===Nonce check===//
630 update_option('wpvr_black_friday_notice', '1');
631 }
632
633 /**
634 * Dismiss black friday notice
635 */
636 function dismiss_black_friday_notice(){
637 if( !current_user_can( 'manage_options' ) ){
638 wp_send_json_error( array( 'message' => 'Unauthorized user' ), 403 );
639 return;
640 }
641 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'wpvr')) {
642 wp_die(__('Permission check failed', 'wpvr'));
643 }
644 update_option('_wpvr_eid_al_adha_2024', 'yes');
645 echo json_encode(['success' => true,]);
646 wp_die();
647 }
648
649 /**
650 * Handles the creation of a contact via a webhook.
651 *
652 * This function validates the nonce, sanitizes and validates the input fields,
653 * and then creates a new contact using the WPVR_Create_Contact class.
654 *
655 * @since 8.4.10
656 */
657 function wpvr_create_contact(){
658 if( !current_user_can( 'manage_options' ) ){
659 wp_send_json_error( array( 'message' => 'Unauthorized user' ), 403 );
660 return;
661 }
662 $nonce = filter_input(INPUT_POST, 'security', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
663 $nonce = !empty( $nonce ) ? $nonce : null;
664 if ( !wp_verify_nonce( $nonce, 'wpvr' ) ) {
665 wp_send_json_error( array( 'message' => 'Invalid nonce' ), 400 );
666 return;
667 }
668
669 $name = filter_input(INPUT_POST, 'name', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
670 $industry = filter_input(INPUT_POST, 'industry', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
671 $email = filter_input(INPUT_POST, 'email', FILTER_SANITIZE_EMAIL);
672 $opt_in = filter_input(INPUT_POST, 'opt_in', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
673
674 $name = !empty($name) ? $name: '';
675 $industry = !empty($industry ) ? $industry : '';
676 $email = !empty( $email ) ? $email : '';
677
678 if ( empty( $email ) ) {
679 wp_send_json_error( array( 'message' => __('Email is required', 'rex-product-feed') ), 400 );
680 }elseif( !is_email( $email ) ){
681 wp_send_json_error( array( 'message' => __('Email is invalid', 'rex-product-feed') ), 400 );
682 }
683
684 $create_contact_instance = new WPVR_Create_Contact( $email, $name, $industry );
685 $response = $create_contact_instance->create_contact_via_webhook();
686
687 update_option('wpvr_posthog_access_enabled', $opt_in);
688
689
690 if ( $response ) {
691 wp_send_json_success( array( 'message' => __('Contact created successfully', 'wpvr') ), 200 );
692 } else {
693 wp_send_json_error( array( 'message' => __('Failed to create contact', 'wpvr') ), 500 );
694 }
695 }
696
697 /**
698 * Saves the general settings for the WPVR plugin.
699 *
700 * This function handles the nonce verification, sanitizes the input fields,
701 * and updates the options in the database. It responds with a JSON success or error message.
702 *
703 * @since 8.4.10
704 */
705 function wpvr_save_general_settings(){
706
707 if ( ! current_user_can( 'manage_options' ) ) {
708 wp_send_json_error( array( 'message' => 'Unauthorized user' ), 403 );
709 return;
710 }
711
712 $nonce = filter_input(INPUT_POST, 'security', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
713 $nonce = !empty( $nonce ) ? $nonce : null; // phpcs:ignore
714 if ( !wp_verify_nonce( $nonce, 'wpvr' ) ) {
715 wp_send_json_error( array( 'message' => 'Invalid nonce' ), 400 );
716 return;
717 }
718
719 $is_mobile_media_resize = filter_input(INPUT_POST, 'media_resizer', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
720 $convert_to_webp = filter_input(INPUT_POST, 'convert_to_webp', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
721 $vr_glass_support = filter_input(INPUT_POST, 'vr_glass_support', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
722
723 update_option('mobile_media_resize', $is_mobile_media_resize);
724 update_option('wpvr_webp_conversion', $convert_to_webp);
725 update_option('wpvr_cardboard_disable', $vr_glass_support);
726
727 wp_send_json_success( array( 'message' => __('General setting data successfully saved.', 'wpvr') ), 200 );
728 }
729
730
731 /**
732 * AJAX handler to persist opt-in toggle value
733 *
734 */
735 public function wpvr_save_opt_in_toggle() {
736 if ( ! current_user_can( 'manage_options' ) ) {
737 wp_send_json_error( array( 'message' => 'Unauthorized user' ), 403 );
738 return;
739 }
740
741 $nonce = isset($_POST['security']) ? sanitize_text_field($_POST['security']) : '';
742 if ( !wp_verify_nonce( $nonce, 'wpvr' ) ) {
743 wp_send_json_error( array( 'message' => 'Invalid nonce' ), 400 );
744 return;
745 }
746
747 $opt_in = isset($_POST['opt_in']) ? sanitize_text_field($_POST['opt_in']) : '0';
748 $consent_state = '1' === $opt_in ? 'yes' : 'no';
749
750 update_option('wpvr_opt_in_toggle', $opt_in);
751 update_option('wpvr_allow_tracking', $consent_state);
752
753 if ( 'yes' === $consent_state ) {
754 $this->wpvr_create_contact_for_current_user();
755 }
756
757 if ( function_exists( 'linno_telemetry' ) && defined( 'WPVR_FILE' ) ) {
758 $telemetry_client = linno_telemetry( WPVR_FILE );
759
760 if ( $telemetry_client && method_exists( $telemetry_client, 'set_optin_state' ) ) {
761 $telemetry_client->set_optin_state( $consent_state );
762 } elseif ( function_exists( 'linno_telemetry_sync_consent_state' ) ) {
763 linno_telemetry_sync_consent_state( WPVR_FILE );
764 }
765 }
766
767 // Fire after SDK consent is fully synced so consent-gated events can queue.
768 if ( 'yes' === $consent_state ) {
769 do_action( 'wpvr_telemetry_consent_granted' );
770 }
771
772 wp_send_json_success( array( 'message' => __('Opt-in value saved.', 'wpvr') ), 200 );
773 }
774
775
776 /**
777 * Create webhook contact from current user after consent.
778 *
779 * @return void
780 */
781 private function wpvr_create_contact_for_current_user() {
782 $current_user = wp_get_current_user();
783 if ( ! $current_user || empty( $current_user->user_email ) ) {
784 return;
785 }
786
787 $email = sanitize_email( $current_user->user_email );
788 if ( ! is_email( $email ) ) {
789 return;
790 }
791
792 $name = sanitize_text_field( $current_user->display_name );
793 if ( empty( $name ) ) {
794 $name = sanitize_text_field( $current_user->user_login );
795 }
796
797 $industry = sanitize_text_field( get_option( 'wpvr_industry_name', '' ) );
798
799 $create_contact_instance = new WPVR_Create_Contact( $email, $name, $industry );
800 $create_contact_instance->create_contact_via_webhook();
801 }
802
803
804 /**
805 * Fetch template tour object from remote API
806 *
807 * @since 8.5.48
808 */
809 public function wpvr_fetch_template() {
810 if ( ! current_user_can( 'manage_options' ) ) {
811 wp_send_json_error( array( 'message' => 'Unauthorized user' ), 403 );
812 return;
813 }
814
815 $nonce = isset($_POST['security']) ? sanitize_text_field($_POST['security']) : '';
816 if ( !wp_verify_nonce( $nonce, 'wpvr' ) ) {
817 wp_send_json_error( array( 'message' => 'Invalid nonce' ), 400 );
818 return;
819 }
820
821 $industry = isset($_POST['industry']) ? sanitize_text_field($_POST['industry']) : 'real-estate';
822
823 // Static industry to remote tour ID mapping
824 $industry_id_map = array(
825 'exhibitions' => 2140,
826 'offices' => 2145,
827 'real-estate' => 2147,
828 'hotel' => 2149,
829 'ecommerce' => 2151,
830 'showrooms' => 2153,
831 'school' => 2155,
832 );
833
834 // Get source tour ID for the selected industry
835 $source_tour_id = isset($industry_id_map[$industry]) ? $industry_id_map[$industry] : 2147;
836
837 // Build API URL with source tour ID
838 $api_url = 'https://showcase.rextheme.com/wp-json/wpvr/v1/tour/' . intval($source_tour_id);
839 $api_url = apply_filters('wpvr_template_api_url', $api_url, $industry, $source_tour_id);
840 $response = wp_remote_get($api_url, array(
841 'timeout' => 30,
842 'headers' => array(
843 'Content-Type' => 'application/json',
844 'Accept' => 'application/json',
845 ),
846 ));
847
848 if ( is_wp_error( $response ) ) {
849 wp_send_json_error( array( 'message' => 'Failed to fetch template: ' . $response->get_error_message() ) );
850 return;
851 }
852
853 $status_code = wp_remote_retrieve_response_code( $response );
854 if ( $status_code !== 200 ) {
855 wp_send_json_error( array( 'message' => 'Template not found (HTTP ' . $status_code . ')' ) );
856 return;
857 }
858
859 $body = wp_remote_retrieve_body( $response );
860 $api_data = json_decode( $body, true );
861
862 if ( ! $api_data || ! is_array( $api_data ) ) {
863 wp_send_json_error( array( 'message' => 'Invalid template data received' ) );
864 return;
865 }
866
867 $remote_meta = array();
868 if ( isset( $api_data['meta_data'] ) && is_array( $api_data['meta_data'] ) ) {
869 $remote_meta = $api_data['meta_data'];
870 } elseif ( isset( $api_data['meta'] ) && is_array( $api_data['meta'] ) ) {
871 $remote_meta = $api_data['meta'];
872 }
873
874 $panodata = array();
875 if ( isset( $remote_meta['panodata'] ) ) {
876 $panodata = $this->wpvr_normalize_panodata( $remote_meta['panodata'] );
877 }
878 if ( empty( $panodata ) && isset( $api_data['panodata'] ) ) {
879 $panodata = $this->wpvr_normalize_panodata( $api_data['panodata'] );
880 }
881
882 if ( empty( $panodata ) ) {
883 wp_send_json_error( array( 'message' => 'Template panodata missing in API response' ) );
884 return;
885 }
886
887 $title = isset( $api_data['title'] ) && ! empty( $api_data['title'] )
888 ? sanitize_text_field( $api_data['title'] )
889 : 'My Virtual Tour';
890
891 $post_data = array(
892 'post_title' => $title,
893 'post_status' => 'publish',
894 'post_type' => 'wpvr_item',
895 'post_author' => get_current_user_id(),
896 );
897
898 $post_id = wp_insert_post( $post_data );
899 if ( is_wp_error( $post_id ) ) {
900 wp_send_json_error( array( 'message' => 'Failed to create tour: ' . $post_id->get_error_message() ) );
901 return;
902 }
903
904 $panodata = $this->wpvr_import_scene_attachments_to_media( $panodata, $post_id );
905 $panodata['panoid'] = 'pano' . $post_id;
906
907 // Keep meta panodata in sync with imported local scene URLs
908 if ( ! is_array( $remote_meta ) ) {
909 $remote_meta = array();
910 }
911 $remote_meta['panodata'] = $panodata;
912
913 update_post_meta( $post_id, 'panodata', $panodata );
914 update_post_meta( $post_id, 'wpvr_created_from_wizard', true );
915 update_post_meta( $post_id, 'wpvr_wizard_industry', $industry );
916
917 if ( ! empty( $remote_meta ) ) {
918 foreach ( $remote_meta as $meta_key => $meta_value ) {
919 $sanitized_key = sanitize_key( $meta_key );
920 if ( empty( $sanitized_key ) || 'panodata' === $sanitized_key ) {
921 continue;
922 }
923
924 if ( is_array( $meta_value ) ) {
925 update_post_meta( $post_id, $sanitized_key, $meta_value );
926 } else {
927 update_post_meta( $post_id, $sanitized_key, sanitize_text_field( $meta_value ) );
928 }
929 }
930 }
931
932 $template_data = array(
933 'industry' => $industry,
934 'template_id' => $source_tour_id,
935 'post_id' => $post_id,
936 'edit_url' => admin_url( 'post.php?action=edit&post=' . $post_id ),
937 'view_url' => get_permalink( $post_id ),
938 'panodata' => $panodata,
939 'meta' => $remote_meta,
940 );
941
942 if ( isset( $panodata['panodata']['scene-list']['1']['scene-attachment-url'] ) ) {
943 $template_data['image_url'] = esc_url_raw( $panodata['panodata']['scene-list']['1']['scene-attachment-url'] );
944 } elseif ( isset( $api_data['image_url'] ) ) {
945 $template_data['image_url'] = esc_url_raw( $api_data['image_url'] );
946 } elseif ( isset( $api_data['featured_image'] ) ) {
947 $template_data['image_url'] = esc_url_raw( $api_data['featured_image'] );
948 }
949
950 do_action('rex_wpvr_tour_saved', $post_id);
951
952 wp_send_json_success( array( 'template' => $template_data ) );
953 }
954
955 /**
956 * Import scene attachment URLs into media library and replace URLs in panodata.
957 *
958 * @param array $panodata Panodata structure.
959 * @param int $post_id Target post ID.
960 *
961 * @return array
962 */
963 private function wpvr_import_scene_attachments_to_media( $panodata, $post_id ) {
964 if ( empty( $panodata['panodata']['scene-list'] ) || ! is_array( $panodata['panodata']['scene-list'] ) ) {
965 return $panodata;
966 }
967
968 require_once( ABSPATH . 'wp-admin/includes/file.php' );
969 require_once( ABSPATH . 'wp-admin/includes/media.php' );
970 require_once( ABSPATH . 'wp-admin/includes/image.php' );
971
972 $scene_image_keys = array(
973 'scene-attachment-url',
974 'scene-attachment-url-face0',
975 'scene-attachment-url-face1',
976 'scene-attachment-url-face2',
977 'scene-attachment-url-face3',
978 'scene-attachment-url-face4',
979 'scene-attachment-url-face5',
980 );
981
982 foreach ( $panodata['panodata']['scene-list'] as $scene_key => $scene ) {
983 if ( ! is_array( $scene ) ) {
984 continue;
985 }
986
987 foreach ( $scene_image_keys as $image_key ) {
988 if ( empty( $scene[ $image_key ] ) || ! is_string( $scene[ $image_key ] ) ) {
989 continue;
990 }
991
992 $source_url = esc_url_raw( $scene[ $image_key ] );
993 if ( empty( $source_url ) ) {
994 continue;
995 }
996
997 $attachment_id = media_sideload_image( $source_url, $post_id, null, 'id' );
998 if ( is_wp_error( $attachment_id ) ) {
999 continue;
1000 }
1001
1002 $local_url = wp_get_attachment_url( $attachment_id );
1003 if ( ! empty( $local_url ) ) {
1004 $panodata['panodata']['scene-list'][ $scene_key ][ $image_key ] = esc_url_raw( $local_url );
1005 }
1006 }
1007 }
1008
1009 return $panodata;
1010 }
1011
1012 /**
1013 * Normalize panodata payloads from array/serialized/json values.
1014 *
1015 * @param mixed $raw_panodata Panodata from remote API/meta.
1016 *
1017 * @return array
1018 */
1019 private function wpvr_normalize_panodata( $raw_panodata ) {
1020 if ( is_array( $raw_panodata ) ) {
1021 return $raw_panodata;
1022 }
1023
1024 if ( is_string( $raw_panodata ) && '' !== $raw_panodata ) {
1025 $unserialized = maybe_unserialize( $raw_panodata );
1026 if ( is_array( $unserialized ) ) {
1027 return $unserialized;
1028 }
1029
1030 $decoded_json = json_decode( $raw_panodata, true );
1031 if ( is_array( $decoded_json ) ) {
1032 return $decoded_json;
1033 }
1034 }
1035
1036 return array();
1037 }
1038
1039 /**
1040 * Upload image to WordPress media library
1041 *
1042 * @since 8.5.48
1043 */
1044 public function wpvr_upload_image() {
1045 if ( ! current_user_can( 'upload_files' ) ) {
1046 wp_send_json_error( array( 'message' => 'Unauthorized user' ), 403 );
1047 return;
1048 }
1049
1050 $nonce = isset($_POST['security']) ? sanitize_text_field($_POST['security']) : '';
1051 if ( !wp_verify_nonce( $nonce, 'wpvr' ) ) {
1052 wp_send_json_error( array( 'message' => 'Invalid nonce' ), 400 );
1053 return;
1054 }
1055
1056 if ( ! isset( $_FILES['image'] ) || empty( $_FILES['image']['tmp_name'] ) ) {
1057 wp_send_json_error( array( 'message' => 'No file uploaded' ) );
1058 return;
1059 }
1060
1061 // Validate file type
1062 $file_type = wp_check_filetype( $_FILES['image']['name'] );
1063 $allowed_types = array( 'jpg', 'jpeg', 'png', 'webp' );
1064 if ( ! in_array( strtolower( $file_type['ext'] ), $allowed_types ) ) {
1065 wp_send_json_error( array( 'message' => 'Invalid file type. Only JPG, PNG, and WEBP are allowed.' ) );
1066 return;
1067 }
1068
1069 // Validate file size (max 50MB)
1070 if ( $_FILES['image']['size'] > 50 * 1024 * 1024 ) {
1071 wp_send_json_error( array( 'message' => 'File size must be less than 50MB' ) );
1072 return;
1073 }
1074
1075 require_once( ABSPATH . 'wp-admin/includes/file.php' );
1076 require_once( ABSPATH . 'wp-admin/includes/media.php' );
1077 require_once( ABSPATH . 'wp-admin/includes/image.php' );
1078
1079 $upload = wp_handle_upload( $_FILES['image'], array( 'test_form' => false ) );
1080
1081 if ( isset( $upload['error'] ) ) {
1082 wp_send_json_error( array( 'message' => $upload['error'] ) );
1083 return;
1084 }
1085
1086 $attachment = array(
1087 'post_mime_type' => $upload['type'],
1088 'post_title' => sanitize_file_name( pathinfo( $_FILES['image']['name'], PATHINFO_FILENAME ) ),
1089 'post_content' => '',
1090 'post_status' => 'inherit'
1091 );
1092
1093 $attach_id = wp_insert_attachment( $attachment, $upload['file'] );
1094 $attach_data = wp_generate_attachment_metadata( $attach_id, $upload['file'] );
1095 wp_update_attachment_metadata( $attach_id, $attach_data );
1096
1097 $image_url = wp_get_attachment_url( $attach_id );
1098
1099 wp_send_json_success( array(
1100 'attachment_id' => $attach_id,
1101 'url' => $image_url,
1102 'message' => 'Image uploaded successfully'
1103 ) );
1104 }
1105
1106 /**
1107 * Create tour from wizard data
1108 *
1109 * @since 8.5.48
1110 */
1111 public function wpvr_create_tour_from_wizard() {
1112 if ( ! current_user_can( 'edit_posts' ) ) {
1113 wp_send_json_error( array( 'message' => 'Unauthorized user' ), 403 );
1114 return;
1115 }
1116
1117 $nonce = isset($_POST['security']) ? sanitize_text_field($_POST['security']) : '';
1118 if ( !wp_verify_nonce( $nonce, 'wpvr' ) ) {
1119 wp_send_json_error( array( 'message' => 'Invalid nonce' ), 400 );
1120 return;
1121 }
1122
1123 $panodata = isset($_POST['panodata']) ? json_decode( stripslashes( $_POST['panodata'] ), true ) : array();
1124 $title = isset($_POST['title']) ? sanitize_text_field($_POST['title']) : 'My Virtual Tour';
1125 $industry = isset($_POST['industry']) ? sanitize_text_field($_POST['industry']) : 'real-estate';
1126 $existing_post_id = isset($_POST['existing_post_id']) ? absint($_POST['existing_post_id']) : 0;
1127
1128 if ( empty( $panodata ) ) {
1129 wp_send_json_error( array( 'message' => 'Panodata is required' ) );
1130 return;
1131 }
1132
1133 if ( $existing_post_id > 0 ) {
1134 $existing_post = get_post( $existing_post_id );
1135 if ( ! $existing_post || 'wpvr_item' !== $existing_post->post_type || ! current_user_can( 'edit_post', $existing_post_id ) ) {
1136 wp_send_json_error( array( 'message' => 'Invalid existing tour ID' ) );
1137 return;
1138 }
1139
1140 $post_id = $existing_post_id;
1141 wp_update_post(
1142 array(
1143 'ID' => $post_id,
1144 'post_title' => $title,
1145 'post_status' => 'publish',
1146 )
1147 );
1148 } else {
1149 // Create new post
1150 $post_data = array(
1151 'post_title' => $title,
1152 'post_status' => 'publish',
1153 'post_type' => 'wpvr_item',
1154 'post_author' => get_current_user_id(),
1155 );
1156
1157 $post_id = wp_insert_post( $post_data );
1158
1159 if ( is_wp_error( $post_id ) ) {
1160 wp_send_json_error( array( 'message' => 'Failed to create tour: ' . $post_id->get_error_message() ) );
1161 return;
1162 }
1163 }
1164
1165 // Enforce local media URLs before final save/update
1166 $panodata = $this->wpvr_import_scene_attachments_to_media( $panodata, $post_id );
1167
1168 // Set panoid as pano{post_id} in panodata
1169 $panodata['panoid'] = 'pano' . $post_id;
1170
1171 // Normalize autoLoad to boolean so Pannellum's strict === true check passes.
1172 if ( isset( $panodata['autoLoad'] ) ) {
1173 $panodata['autoLoad'] = (bool) $panodata['autoLoad'];
1174 }
1175
1176 // Save panodata as post meta
1177 update_post_meta( $post_id, 'panodata', $panodata );
1178
1179 // Mark as created from wizard
1180 update_post_meta( $post_id, 'wpvr_created_from_wizard', true );
1181 update_post_meta( $post_id, 'wpvr_wizard_industry', $industry );
1182
1183 // Save template meta fields if provided (dynamic meta from API)
1184 $template_meta = isset($_POST['templateMeta']) ? json_decode( stripslashes( $_POST['templateMeta'] ), true ) : array();
1185 if ( ! empty( $template_meta ) && is_array( $template_meta ) ) {
1186 foreach ( $template_meta as $meta_key => $meta_value ) {
1187 // Sanitize meta key to ensure it's a valid meta key
1188 $sanitized_key = sanitize_key( $meta_key );
1189 if ( ! empty( $sanitized_key ) && 'panodata' !== $sanitized_key ) {
1190 // Handle different value types
1191 if ( is_array( $meta_value ) ) {
1192 update_post_meta( $post_id, $sanitized_key, $meta_value );
1193 } else {
1194 update_post_meta( $post_id, $sanitized_key, sanitize_text_field( $meta_value ) );
1195 }
1196 }
1197 }
1198 }
1199
1200 // Trigger tour saved action for telemetry
1201 do_action('rex_wpvr_tour_saved', $post_id);
1202 do_action( 'wpvr_setup_wizard_completed_event', $industry );
1203
1204 // Persist industry selection for telemetry (aha event fires later from consent handler).
1205 update_option( 'wpvr_industry_name', sanitize_text_field( $industry ), false );
1206
1207 // Mark wizard as permanently done so the onboarding notice is suppressed.
1208 update_option( 'wpvr_wizard_onboarding_done', '1', false );
1209
1210 wp_send_json_success( array(
1211 'post_id' => $post_id,
1212 'edit_url' => admin_url( 'post.php?action=edit&post=' . $post_id ),
1213 'view_url' => get_permalink( $post_id ),
1214 'message' => 'Tour created successfully'
1215 ) );
1216 }
1217
1218 }
1219