PluginProbe
WP VR – 360 Panorama and Virtual Tour Builder / 9.0.0
WP VR – 360 Panorama and Virtual Tour Builder v9.0.0
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 8.5.40 All 220 releases
wpvr / wpvr.php

wpvr.php in WP VR – 360 Panorama and Virtual Tour Builder 9.0.0, at wpvr.php

660 lines 23.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 use RexTheme\WPVR\Tracker\WPVRLinnoTelemetry;
4
5 /**
6 * The plugin bootstrap file
7 *
8 * This file is read by WordPress to generate the plugin information in the plugin
9 * admin area. This file also includes all of the dependencies used by the plugin,
10 * registers the activation and deactivation functions, and defines a function
11 * that starts the plugin.
12 *
13 * @link http://rextheme.com/
14 * @since 7.3.6
15 * @package Wpvr
16 *
17 * @wordpress-plugin
18 * Plugin Name: WP VR - 360 Panorama and Virtual Tour Builder
19 * Plugin URI: https://rextheme.com/wpvr/
20 * Description: WP VR - 360 Panorama and virtual tour creator is a customized panaroma & virtual builder tool for your website.
21 * Version: 9.0.0
22 * Tested up to: 6.9
23 * Author: Rextheme
24 * Author URI: http://rextheme.com/
25 * License: GPL-2.0+
26 * License URI: http://www.gnu.org/licenses/gpl-2.0.txt
27 * Text Domain: wpvr
28 * Domain Path: /languages
29 */
30
31 // If this file is called directly, abort.
32 if (!defined('WPINC')) {
33 die;
34 }
35
36 define('WPVR_VERSION', '9.0.0');
37 define('WPVR_FILE', __FILE__);
38 define("WPVR_PLUGIN_DIR_URL", plugin_dir_url(__FILE__).'legacy/');
39 define("WPVR_PLUGIN_DIR_PATH", plugin_dir_path(__FILE__).'legacy/');
40 define("WPVR_PLUGIN_LEGACY_DIR_PATH", plugin_dir_path(__FILE__) . 'legacy/');
41 define("WPVR_PLUGIN_PUBLIC_DIR_URL", plugin_dir_url(__FILE__) . 'legacy/public/');
42 define('WPVR_BASE', plugin_basename(WPVR_FILE));
43 define('WPVR_DEV_MODE', false);
44 define('WPVR_JS_PATH', plugin_dir_url(__FILE__) . 'legacy/admin/js/');
45 define('WPVR_CSS_PATH', plugin_dir_url(__FILE__) . 'legacy/admin/css/');
46 define('WPVR_ASSET_PATH', plugin_dir_url(__FILE__) . 'legacy/admin/');
47
48 $wpvr_composer_autoload = plugin_dir_path( __FILE__ ) . 'vendor/autoload.php';
49 if ( file_exists( $wpvr_composer_autoload ) ) {
50 require_once $wpvr_composer_autoload;
51 }
52
53 if ( ! defined( 'WPVR_TELEMETRY_API_KEY' ) ) {
54 define( 'WPVR_TELEMETRY_API_KEY', 'phc_amk3VQz1P5ZqZOMRRoWM1B41QMFpf3hu8pg7yRfzSXW' );
55 }
56 if ( ! defined( 'WPVR_TELEMETRY_API_SECRET' ) ) {
57 define( 'WPVR_TELEMETRY_API_SECRET', 'sec_dfb2ab8e84391ae7a0f9' );
58 }
59 if ( ! defined( 'WPVR_TELEMETRY_HOST' ) ) {
60 define( 'WPVR_TELEMETRY_HOST', 'https://eu.i.posthog.com' );
61 }
62
63 if ( ! function_exists( 'activate_wpvr' ) ) {
64 /**
65 * The code that runs during plugin activation.
66 */
67 function activate_wpvr() {
68 require_once WPVR_PLUGIN_LEGACY_DIR_PATH . 'includes/class-wpvr-activator.php';
69 Wpvr_Activator::activate();
70
71 // Trigger plugin activation tracking
72 do_action( 'wpvr_plugin_activated' );
73 }
74 }
75
76 if ( ! function_exists( 'deactivate_wpvr' ) ) {
77 /**
78 * The code that runs during plugin deactivation.
79 */
80 function deactivate_wpvr() {
81 require_once WPVR_PLUGIN_LEGACY_DIR_PATH . 'includes/class-wpvr-deactivator.php';
82 Wpvr_Deactivator::deactivate();
83
84 // Trigger plugin deactivation tracking
85 do_action( 'wpvr_plugin_deactivated' );
86 }
87 }
88
89 register_activation_hook( WPVR_FILE, 'activate_wpvr' );
90 register_deactivation_hook( WPVR_FILE, 'deactivate_wpvr' );
91
92 if ( class_exists( WPVRLinnoTelemetry::class ) ) {
93 new WPVRLinnoTelemetry();
94 }
95
96 if ( ! function_exists( 'wpvr_is_pro_active' ) ) {
97 /**
98 * Return the effective WP VR Pro entitlement state.
99 *
100 * A loaded Pro plugin alone is not enough; its license must also be valid.
101 */
102 function wpvr_is_pro_active(): bool {
103 return defined( 'WPVR_PRO_VERSION' )
104 && get_option( 'wpvr_edd_license_status' ) === 'valid'
105 && (bool) apply_filters( 'is_wpvr_pro_active', false );
106 }
107 }
108
109 if ( ! function_exists( 'wpvr_get_effective_panodata' ) ) {
110 /**
111 * Build a runtime-only Free-plan view of saved tour data.
112 *
113 * The stored meta is never changed, so reactivating Pro restores every value.
114 * Also normalizes values written by newer editor clients to the legacy
115 * string format expected by the shared preview and shortcode renderers.
116 */
117 function wpvr_get_effective_panodata( array $postdata ): array {
118 if ( isset( $postdata['scene_navigation'] ) ) {
119 $postdata['scene_navigation'] = in_array( $postdata['scene_navigation'], [ true, 1, '1', 'on' ], true ) ? 'on' : 'off';
120 }
121
122 if ( wpvr_is_pro_active() ) {
123 return $postdata;
124 }
125
126 $false_keys = [
127 'gyro', 'deviceorientationcontrol', 'compass', 'vrgallery',
128 'vrgallery_title', 'vrgallery_icon_size', 'vrgallery_display',
129 ];
130 foreach ( $false_keys as $key ) {
131 $postdata[ $key ] = false;
132 }
133
134 $off_keys = [
135 'scene_navigation', 'sceneAnimation', 'bg_music',
136 'autoplay_bg_music', 'loop_bg_music', 'explainerSwitch',
137 'cpLogoSwitch', 'genericform', 'calltoaction', 'customcss_enable',
138 'floorplan-enabled', 'floorplan-compass',
139 'floor_plan_tour_enabler', 'floor_plan_direction_indicator',
140 'bg_tour_enabler', 'video-autoplay', 'video-loop',
141 ];
142 foreach ( $off_keys as $key ) {
143 $postdata[ $key ] = 'off';
144 }
145
146 $postdata['tourLayout'] = [
147 'layout' => 'default',
148 'layout_icon_bg_color' => '#5a536e',
149 'layout_icon_color' => '#ffffff',
150 ];
151 $postdata['draggable'] = true;
152 $postdata['mouseZoom'] = true;
153 $postdata['diskeyboard'] = true;
154 $postdata['keyboardzoom'] = true;
155 $postdata['hfov'] = '';
156 $postdata['maxHfov'] = '';
157 $postdata['minHfov'] = '';
158
159 if ( ( $postdata['tour-type'] ?? '' ) === 'street-view' || isset( $postdata['streetviewdata'] ) ) {
160 $postdata['tour-type'] = 'image';
161 $postdata['streetview'] = 'off';
162 $postdata['streetviewurl'] = '';
163 unset( $postdata['streetviewdata'] );
164 }
165
166 $custom_control = isset( $postdata['customcontrol'] ) && is_array( $postdata['customcontrol'] )
167 ? $postdata['customcontrol']
168 : [];
169 foreach ( [
170 'panupSwitch', 'panDownSwitch', 'panLeftSwitch', 'panRightSwitch',
171 'panZoomInSwitch', 'panZoomOutSwitch', 'panFullscreenSwitch',
172 'gyroscopeSwitch', 'backToHomeSwitch',
173 ] as $key ) {
174 $custom_control[ $key ] = 'off';
175 }
176 $postdata['customcontrol'] = $custom_control;
177
178 if ( isset( $postdata['panodata']['scene-list'] ) && is_array( $postdata['panodata']['scene-list'] ) ) {
179 foreach ( $postdata['panodata']['scene-list'] as &$scene ) {
180 if ( ! is_array( $scene ) ) {
181 continue;
182 }
183
184 if ( ( $scene['scene-type'] ?? 'equirectangular' ) === 'cubemap' ) {
185 $scene['scene-type'] = 'equirectangular';
186 }
187
188 foreach ( [ 'ptyscene', 'cvgscene', 'chgscene', 'czscene' ] as $key ) {
189 $scene[ $key ] = 'off';
190 }
191 foreach ( [
192 'scene-author', 'scene-author-url', 'scene-vaov', 'scene-haov',
193 'scene-vertical-offset', 'scene-pitch', 'scene-yaw',
194 'scene-maxpitch', 'scene-minpitch', 'scene-maxyaw', 'scene-minyaw',
195 'scene-zoom', 'scene-maxzoom', 'scene-minzoom',
196 ] as $key ) {
197 $scene[ $key ] = '';
198 }
199 for ( $face = 0; $face < 6; $face++ ) {
200 $scene[ 'scene-attachment-url-face' . $face ] = '';
201 }
202
203 if ( isset( $scene['hotspot-list'] ) && is_array( $scene['hotspot-list'] ) ) {
204 foreach ( $scene['hotspot-list'] as &$hotspot ) {
205 if ( ! is_array( $hotspot ) ) {
206 continue;
207 }
208 $hotspot['hotspot-customclass-pro'] = 'none';
209 $hotspot['hotspot-blink'] = 'off';
210 $hotspot['hotspot-shape'] = 'round';
211 $hotspot['hotspot-border'] = 'off';
212 $hotspot['hotspot-scene-pitch'] = '';
213 $hotspot['hotspot-scene-yaw'] = '';
214 }
215 unset( $hotspot );
216 }
217 }
218 unset( $scene );
219 }
220
221 return $postdata;
222 }
223 }
224
225 if ( ! function_exists( 'wpvr_tour_uses_pro_features' ) ) {
226 /**
227 * Determine whether a saved tour contains enabled WP VR Pro features.
228 *
229 * @param mixed $postdata Saved tour data.
230 * @return bool
231 */
232 function wpvr_tour_uses_pro_features( $postdata ) {
233 if ( ! is_array( $postdata ) ) {
234 return false;
235 }
236
237 $is_on = static function ( $value ) {
238 return in_array( $value, array( true, 1, '1', 'on', 'true' ), true );
239 };
240
241 if (
242 isset( $postdata['streetviewdata'] )
243 || ( isset( $postdata['streetview'] ) && $is_on( $postdata['streetview'] ) )
244 || ! empty( $postdata['streetviewurl'] )
245 ) {
246 return true;
247 }
248
249 foreach ( array(
250 'bg_tour_enabler', 'floorplan-enabled', 'floor_plan_tour_enabler',
251 'floor_plan_compass', 'floor_plan_direction_indicator',
252 'gyro', 'deviceorientationcontrol', 'compass', 'vrgallery',
253 'vrgallery_title', 'vrgallery_icon_size', 'vrgallery_display',
254 'scene_navigation', 'sceneAnimation', 'bg_music',
255 'autoplay_bg_music', 'loop_bg_music', 'explainerSwitch',
256 'cpLogoSwitch', 'genericform', 'calltoaction', 'customcss_enable',
257 ) as $key ) {
258 if ( isset( $postdata[ $key ] ) && $is_on( $postdata[ $key ] ) ) {
259 return true;
260 }
261 }
262
263 if (
264 ! empty( $postdata['hfov'] )
265 || ! empty( $postdata['maxHfov'] )
266 || ! empty( $postdata['minHfov'] )
267 ) {
268 return true;
269 }
270
271 $tour_layout = isset( $postdata['tourLayout'] ) ? $postdata['tourLayout'] : 'default';
272 $tour_layout = is_array( $tour_layout )
273 ? ( isset( $tour_layout['layout'] ) ? $tour_layout['layout'] : 'default' )
274 : $tour_layout;
275 if ( $tour_layout !== '' && $tour_layout !== 'default' ) {
276 return true;
277 }
278
279 $custom_control = isset( $postdata['customcontrol'] ) && is_array( $postdata['customcontrol'] )
280 ? $postdata['customcontrol']
281 : array();
282 foreach ( array(
283 'panupSwitch', 'panDownSwitch', 'panLeftSwitch', 'panRightSwitch',
284 'panZoomInSwitch', 'panZoomOutSwitch', 'panFullscreenSwitch',
285 'gyroscopeSwitch', 'backToHomeSwitch',
286 ) as $key ) {
287 if ( isset( $custom_control[ $key ] ) && $is_on( $custom_control[ $key ] ) ) {
288 return true;
289 }
290 }
291
292 $scenes = isset( $postdata['panodata']['scene-list'] ) ? $postdata['panodata']['scene-list'] : array();
293 if ( ! is_array( $scenes ) ) {
294 return false;
295 }
296
297 $pro_scene_fields = array(
298 'scene-vaov', 'scene-haov', 'scene-vertical-offset', 'scene-pitch',
299 'scene-yaw', 'scene-maxpitch', 'scene-minpitch', 'scene-maxyaw',
300 'scene-minyaw', 'scene-zoom', 'scene-maxzoom', 'scene-minzoom',
301 );
302
303 foreach ( $scenes as $scene ) {
304 if ( ! is_array( $scene ) ) {
305 continue;
306 }
307
308 if ( isset( $scene['scene-type'] ) && 'cubemap' === $scene['scene-type'] ) {
309 return true;
310 }
311
312 foreach ( array( 'ptyscene', 'cvgscene', 'chgscene', 'czscene' ) as $key ) {
313 if ( isset( $scene[ $key ] ) && $is_on( $scene[ $key ] ) ) {
314 return true;
315 }
316 }
317
318 foreach ( $pro_scene_fields as $key ) {
319 if ( isset( $scene[ $key ] ) && '' !== $scene[ $key ] && null !== $scene[ $key ] ) {
320 return true;
321 }
322 }
323
324 $hotspots = isset( $scene['hotspot-list'] ) ? $scene['hotspot-list'] : array();
325 if ( ! is_array( $hotspots ) ) {
326 continue;
327 }
328
329 foreach ( $hotspots as $hotspot ) {
330 if ( ! is_array( $hotspot ) ) {
331 continue;
332 }
333
334 if (
335 ( ! empty( $hotspot['hotspot-customclass-pro'] ) && 'none' !== $hotspot['hotspot-customclass-pro'] )
336 || ( ! empty( $hotspot['hotspot-shape'] ) && 'round' !== $hotspot['hotspot-shape'] )
337 || ( isset( $hotspot['hotspot-border'] ) && $is_on( $hotspot['hotspot-border'] ) )
338 || ! empty( $hotspot['hotspot-scene-pitch'] )
339 || ! empty( $hotspot['hotspot-scene-yaw'] )
340 || ( isset( $hotspot['hotspot-scene-entry-point-mode'] ) && 'custom' === $hotspot['hotspot-scene-entry-point-mode'] )
341 ) {
342 return true;
343 }
344 }
345 }
346
347 return false;
348 }
349 }
350
351 if ( ! function_exists( 'wpvr_get_pro_feature_tour_ids' ) ) {
352 /**
353 * Return IDs of tours that use at least one enabled Pro feature.
354 *
355 * @return array
356 */
357 function wpvr_get_pro_feature_tour_ids() {
358 static $tour_ids = null;
359
360 if ( is_array( $tour_ids ) ) {
361 return $tour_ids;
362 }
363
364 $tour_ids = array();
365 $posts = get_posts( array(
366 'post_type' => 'wpvr_item',
367 'post_status' => 'any',
368 'posts_per_page' => -1,
369 'fields' => 'ids',
370 'no_found_rows' => true,
371 'update_post_meta_cache' => true,
372 'update_post_term_cache' => false,
373 ) );
374
375 foreach ( $posts as $tour_id ) {
376 $postdata = get_post_meta( (int) $tour_id, 'panodata', true );
377 if ( wpvr_tour_uses_pro_features( $postdata ) ) {
378 $tour_ids[] = (int) $tour_id;
379 }
380 }
381
382 return $tour_ids;
383 }
384 }
385
386 if ( ! function_exists( 'wpvr_has_pro_feature_tours' ) ) {
387 function wpvr_has_pro_feature_tours() {
388 return ! empty( wpvr_get_pro_feature_tour_ids() );
389 }
390 }
391
392 if ( ! function_exists( 'wpvr_enqueue_license_notice_assets' ) ) {
393 /**
394 * Enqueue the global license notice styles while Pro is installed and inactive.
395 *
396 * @return void
397 */
398 function wpvr_enqueue_license_notice_assets() {
399 if (
400 ! is_admin()
401 || ! defined( 'WPVR_PRO_VERSION' )
402 || wpvr_is_pro_active()
403 ) {
404 return;
405 }
406
407 wp_enqueue_style(
408 'wpvr-license-notice-fontawesome',
409 WPVR_PLUGIN_DIR_URL . 'admin/lib/fontawesome/css/all.css',
410 array(),
411 WPVR_VERSION
412 );
413 wp_enqueue_style(
414 'wpvr-license-notice',
415 WPVR_PLUGIN_DIR_URL . 'admin/css/wpvr-license-notice.css',
416 array( 'wpvr-license-notice-fontawesome' ),
417 WPVR_VERSION
418 );
419 wp_enqueue_script(
420 'wpvr-license-notice',
421 WPVR_PLUGIN_DIR_URL . 'admin/js/wpvr-license-notice.js',
422 array(),
423 WPVR_VERSION,
424 true
425 );
426 }
427 }
428
429 add_action( 'admin_enqueue_scripts', 'wpvr_enqueue_license_notice_assets', 10 );
430
431 if ( ! function_exists( 'wpvr_render_pro_feature_fallback_notice' ) ) {
432 /**
433 * Render the global Pro license notice and explain the saved-tour fallback.
434 *
435 * @return void
436 */
437 function wpvr_render_pro_feature_fallback_notice() {
438 if (
439 ! is_admin()
440 || ! current_user_can( 'edit_posts' )
441 || ! defined( 'WPVR_PRO_VERSION' )
442 || wpvr_is_pro_active()
443 ) {
444 return;
445 }
446
447 $status = strtolower( (string) get_option( 'wpvr_edd_license_status', 'no' ) );
448 $needs_activation = in_array( $status, array( '', 'no' ), true );
449
450 if ( ! $needs_activation && ! wpvr_has_pro_feature_tours() ) {
451 return;
452 }
453
454 if ( $needs_activation ) {
455 $title = __( 'Please Activate your license key to enjoy the pro features for WPVR', 'wpvr' );
456 $subtitle = '';
457 $action_label = __( 'Activate', 'wpvr' );
458 $action_url = admin_url( 'admin.php?page=wpvrpro' );
459 $action_target = '';
460 $action_icon = 'fa-key';
461 } else {
462 $title = 'expired' === $status
463 ? __( 'Your WPVR Pro license has expired.', 'wpvr' )
464 : __( 'Your WPVR Pro license is not active.', 'wpvr' );
465 $subtitle = __( 'Pro features are no longer available. Upgrade to Pro now to restore access to all premium features.', 'wpvr' );
466 $action_label = __( 'Upgrade to Pro', 'wpvr' );
467 $action_url = 'https://rextheme.com/your-account/#purchase';
468 $action_target = ' target="_blank" rel="noopener noreferrer"';
469 $action_icon = 'fa-crown';
470 }
471
472 $notice_classes = 'notice notice-error is-dismissible wpvr-license-fallback-notice';
473 if ( '' === $subtitle ) {
474 $notice_classes .= ' wpvr-license-fallback-notice--single-line';
475 }
476
477 echo '<div id="wpvr-license-fallback-notice" class="' . esc_attr( $notice_classes ) . '">';
478 echo '<img class="wpvr-license-fallback-notice__icon" src="' . esc_url( WPVR_PLUGIN_DIR_URL . 'admin/images/wpvr-license-warning.svg' ) . '" width="20" height="19" alt="" aria-hidden="true">';
479 echo '<div class="wpvr-license-fallback-notice__content">';
480 echo '<p class="wpvr-license-fallback-notice__title">' . esc_html( $title ) . '</p>';
481 if ( '' !== $subtitle ) {
482 echo '<p class="wpvr-license-fallback-notice__subtitle">' . esc_html( $subtitle ) . '</p>';
483 }
484 echo '</div>';
485 echo '<a class="wpvr-license-fallback-notice__action" href="' . esc_url( $action_url ) . '"' . $action_target . '>';
486 echo '<span class="wpvr-license-fallback-notice__action-icon fa-solid ' . esc_attr( $action_icon ) . '" aria-hidden="true"></span>';
487 echo '<span>' . esc_html( $action_label ) . '</span>';
488 echo '</a>';
489 echo '<button type="button" class="notice-dismiss"><span class="screen-reader-text">' . esc_html__( 'Dismiss this notice.', 'wpvr' ) . '</span></button>';
490 echo '</div>';
491 }
492 }
493
494 add_action( 'admin_notices', 'wpvr_render_pro_feature_fallback_notice', 10 );
495
496 if ( ! function_exists( 'wpvr_add_pro_feature_tour_row_class' ) ) {
497 /**
498 * Add a class to affected tour rows while the Pro entitlement is inactive.
499 *
500 * @param array $classes Existing post classes.
501 * @param array $class Additional classes supplied by WordPress.
502 * @param int $post_id Post ID.
503 * @return array
504 */
505 function wpvr_add_pro_feature_tour_row_class( $classes, $class = array(), $post_id = 0 ) {
506 if (
507 ! is_admin()
508 || ! defined( 'WPVR_PRO_VERSION' )
509 || wpvr_is_pro_active()
510 || 'wpvr_item' !== get_post_type( $post_id )
511 ) {
512 return $classes;
513 }
514
515 $postdata = get_post_meta( (int) $post_id, 'panodata', true );
516 if ( wpvr_tour_uses_pro_features( $postdata ) ) {
517 $classes[] = 'wpvr-pro-feature-tour';
518 }
519
520 return $classes;
521 }
522 }
523
524 add_filter( 'post_class', 'wpvr_add_pro_feature_tour_row_class', 10, 3 );
525
526 require_once plugin_dir_path( __FILE__ ) . 'src/Admin/UiModeSwitcher.php';
527 \RexTheme\WPVR\Admin\UiModeSwitcher::init();
528
529 add_action( 'wp_ajax_wpvr_export_tour', 'wpvr_handle_export_tour' );
530 /**
531 * AJAX handler: return a signed URL for the Pro tour exporter.
532 *
533 * WPVR Pro creates and streams the ZIP through its admin-post endpoint. This
534 * handler authorizes the New UI request and returns that endpoint URL.
535 */
536 function wpvr_handle_export_tour(): void {
537 check_ajax_referer( 'wpvr_export_tour', 'nonce' );
538
539 if ( ! current_user_can( 'edit_posts' ) ) {
540 wp_send_json_error( [ 'message' => 'Insufficient permissions.' ], 403 );
541 }
542
543 if ( ! wpvr_is_pro_active() ) {
544 wp_send_json_error( [ 'message' => 'A valid WP VR Pro license is required.' ], 403 );
545 }
546
547 $tour_id = isset( $_POST['tour_id'] ) ? absint( $_POST['tour_id'] ) : 0;
548 if ( ! $tour_id ) {
549 wp_send_json_error( [ 'message' => 'Missing tour ID.' ] );
550 }
551
552 $post = get_post( $tour_id );
553 if ( ! $post || $post->post_type !== 'wpvr_item' ) {
554 wp_send_json_error( [ 'message' => 'Tour not found.' ] );
555 }
556
557 if ( ! current_user_can( 'edit_post', $tour_id ) ) {
558 wp_send_json_error( [ 'message' => 'You are not allowed to export this tour.' ], 403 );
559 }
560
561 $export_url = add_query_arg(
562 [
563 'action' => 'wpvr_export_tour',
564 'post_id' => $tour_id,
565 '_wpnonce' => wp_create_nonce( 'wpvr_export_tour_' . $tour_id ),
566 ],
567 admin_url( 'admin-post.php' )
568 );
569
570 wp_send_json_success( [ 'url' => $export_url ] );
571 }
572
573 /**
574 * Ensure legacy Street View keys always exist in panodata.
575 *
576 * WPVR Pro reads these keys directly in classic editor screens. Tours created
577 * through newer flows may miss them, which triggers PHP warnings.
578 */
579 function wpvr_backfill_legacy_streetview_keys() {
580 if ( ! is_admin() ) {
581 return;
582 }
583
584 $post_id = isset( $_GET['post'] ) ? absint( $_GET['post'] ) : 0;
585 if ( ! $post_id ) {
586 return;
587 }
588
589 $post = get_post( $post_id );
590 if ( ! $post || $post->post_type !== 'wpvr_item' ) {
591 return;
592 }
593
594 $panodata = get_post_meta( $post_id, 'panodata', true );
595 if ( ! is_array( $panodata ) ) {
596 return;
597 }
598
599 $changed = false;
600
601 if ( ! array_key_exists( 'streetview', $panodata ) ) {
602 $panodata['streetview'] = 'off';
603 $changed = true;
604 }
605
606 if ( ! array_key_exists( 'streetviewurl', $panodata ) ) {
607 $panodata['streetviewurl'] = '';
608 $changed = true;
609 }
610
611 if ( $changed ) {
612 update_post_meta( $post_id, 'panodata', $panodata );
613 }
614 }
615
616 add_action( 'load-post.php', 'wpvr_backfill_legacy_streetview_keys', 1 );
617
618 /**
619 * Register a lightweight PSR-4 autoloader for src/ classes.
620 */
621 function wpvr_register_src_autoloader() {
622 static $loaded = false;
623
624 if ( $loaded ) {
625 return;
626 }
627
628 spl_autoload_register(
629 static function ( $class ) {
630 $prefix = 'RexTheme\\WPVR\\';
631
632 if ( strpos( $class, $prefix ) !== 0 ) {
633 return;
634 }
635
636 $relative_class = substr( $class, strlen( $prefix ) );
637 $file = plugin_dir_path( __FILE__ ) . 'src/' . str_replace( '\\', '/', $relative_class ) . '.php';
638
639 if ( file_exists( $file ) ) {
640 require_once $file;
641 }
642 }
643 );
644
645 $loaded = true;
646 }
647
648 // UI/UX Mode Switch: legacy/classic or latest
649 // Option: wpvr_ui_mode (default: legacy)
650 wpvr_register_src_autoloader();
651
652 $wpvr_ui_mode = get_option('wpvr_ui_mode', 'legacy');
653 if ($wpvr_ui_mode === 'latest') {
654 require_once plugin_dir_path(__FILE__) . 'src/Bootstrap.php';
655 \RexTheme\WPVR\Bootstrap::init();
656 } else {
657 require_once plugin_dir_path(__FILE__) . 'legacy/wpvr.php';
658 new \RexTheme\WPVR\Admin\Admin( false );
659 }
660