PluginProbe
WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress / 9.2.0
WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress v9.2.0
9.2.0 9.1.4 9.1.3 9.1.2 9.1.1 9.1.0 9.0.3 9.0.2 9.0.1 9.0.0 8.5.79 8.5.78 8.5.77 8.5.76 8.5.75 8.5.74 8.5.73 8.5.72 8.5.71 8.5.70 8.5.69 8.5.68 8.5.35 8.5.36 8.5.37 All 224 releases
wpvr / legacy / wpvr.php

wpvr.php in WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress 9.2.0, at legacy/wpvr.php

1,469 lines 55.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 // Legacy bootstrap for WP VR plugin
3 // This file is a direct copy of the original wpvr.php before architecture migration.
4 // Do not edit legacy logic here; only update if the legacy code itself changes.
5
6 if (!defined('WPINC')) {
7 die;
8 }
9
10 require WPVR_PLUGIN_LEGACY_DIR_PATH . 'elementor/elementor.php';
11 require_once WPVR_PLUGIN_LEGACY_DIR_PATH . 'breakdance/breakdance.php';
12 if ( file_exists( dirname( WPVR_PLUGIN_LEGACY_DIR_PATH ) . '/vendor/autoload.php' ) ) {
13 require_once dirname( WPVR_PLUGIN_LEGACY_DIR_PATH ) . '/vendor/autoload.php';
14 } elseif ( file_exists( WPVR_PLUGIN_LEGACY_DIR_PATH . 'vendor/autoload.php' ) ) {
15 require_once WPVR_PLUGIN_LEGACY_DIR_PATH . 'vendor/autoload.php';
16 }
17
18 if ( wp_get_theme('bricks')->exists() && 'bricks' === get_template()) {
19 require_once WPVR_PLUGIN_LEGACY_DIR_PATH . 'bricks/bricks.php';
20 }
21
22
23 /**
24 * Currently plugin version.
25 * Start at version 1.0.0 and use SemVer - https://semver.org
26 * Rename this for your plugin and update it as you release new versions.
27 */
28 define( 'WPVR_WEBHOOK_URL', sanitize_url( 'https://rextheme.com/?mailmint=1&route=webhook&topic=contact&hash=bbd19901-6d42-4ae5-a7a8-01eb1013c553' ) );
29 if ( ! defined( 'WPVR_TELEMETRY_API_KEY' ) ) {
30 define( 'WPVR_TELEMETRY_API_KEY', 'phc_amk3VQz1P5ZqZOMRRoWM1B41QMFpf3hu8pg7yRfzSXW' );
31 }
32 if ( ! defined( 'WPVR_TELEMETRY_API_SECRET' ) ) {
33 define( 'WPVR_TELEMETRY_API_SECRET', 'sec_dfb2ab8e84391ae7a0f9' );
34 }
35 if ( ! defined( 'WPVR_TELEMETRY_HOST' ) ) {
36 define( 'WPVR_TELEMETRY_HOST', 'https://eu.i.posthog.com' );
37 }
38
39
40 if ( ! function_exists( 'activate_wpvr' ) ) {
41 /**
42 * The code that runs during plugin activation.
43 * This action is documented in includes/class-wpvr-activator.php
44 */
45 function activate_wpvr()
46 {
47 require_once WPVR_PLUGIN_LEGACY_DIR_PATH . 'includes/class-wpvr-activator.php';
48 Wpvr_Activator::activate();
49
50 // Trigger plugin activation tracking
51 do_action( 'wpvr_plugin_activated' );
52 }
53 }
54
55 if ( ! function_exists( 'deactivate_wpvr' ) ) {
56 /**
57 * The code that runs during plugin deactivation.
58 * This action is documented in includes/class-wpvr-deactivator.php
59 */
60 function deactivate_wpvr()
61 {
62 require_once WPVR_PLUGIN_LEGACY_DIR_PATH . 'includes/class-wpvr-deactivator.php';
63 Wpvr_Deactivator::deactivate();
64
65 // Trigger plugin deactivation tracking
66 do_action( 'wpvr_plugin_deactivated' );
67 }
68 }
69
70 register_activation_hook( WPVR_FILE, 'activate_wpvr' );
71 register_deactivation_hook( WPVR_FILE, 'deactivate_wpvr' );
72
73 /**
74 * The core plugin class that is used to define internationalization,
75 * admin-specific hooks, and public-facing site hooks.
76 */
77 require WPVR_PLUGIN_LEGACY_DIR_PATH . 'includes/class-wpvr.php';
78
79 // Include banner classes
80 require_once WPVR_PLUGIN_LEGACY_DIR_PATH . 'admin/classes/class-wpvr-occasion-banner.php';
81 require_once WPVR_PLUGIN_LEGACY_DIR_PATH . 'admin/classes/class-wpvr-sells-notification-bar.php';
82 require_once WPVR_PLUGIN_LEGACY_DIR_PATH . 'admin/classes/class-wpvr-first-tour-banner.php';
83 require_once WPVR_PLUGIN_LEGACY_DIR_PATH . 'admin/classes/class-wpvr-onboarding-notice.php';
84 require_once WPVR_PLUGIN_LEGACY_DIR_PATH . 'admin/classes/class-wpvr-new-user-tour.php';
85
86 if ( defined( 'WPB_VC_VERSION' ) ) {
87 require_once WPVR_PLUGIN_LEGACY_DIR_PATH . 'builders/wpbakery/wpvr-loader.php';
88 if ( class_exists( 'Vc_Manager' ) ) {
89 require_once WPVR_PLUGIN_LEGACY_DIR_PATH . 'builders/wpbakery/wpvr-element.php';
90 }
91 }
92
93
94 /**
95 * Begins execution of the plugin.
96 *
97 * Since everything within the plugin is registered via hooks,
98 * then kicking off the plugin from this point in the file does
99 * not affect the page life cycle.
100 *
101 * @since 7.3.6
102 */
103 function run_wpvr()
104 {
105
106 $plugin = new Wpvr();
107 $plugin->run();
108 // new Tracker();
109
110 // black friday banner class initialization
111 // new WPVR_Special_Occasion_Banner(
112 // 'halloween_deal_2025',
113 // '2025-10-01 00:00:01',
114 // '2025-11-05 23:59:59'
115 // );
116
117 if (!defined('WPVR_PRO_VERSION') && 'no' === get_option('wpvr_sell_eid_ul_fitr_2026_notification_bar', 'no')) {
118 new WPVR_Notification_Bar(
119 'Eid_Ul_Fitr_2026',
120 '2026-03-16 00:00:00',
121 '2026-03-24 23:59:59'
122 );
123 }
124
125 // Initialize first tour banner
126 new WPVR_First_Tour_Banner();
127
128 // Initialize listing-page onboarding notice ("Remind me later" flow)
129 new WPVR_Onboarding_Notice();
130
131 // Auto-launch guided tour for truly-new users on the Add New Tour page.
132 new WPVR_New_User_Tour();
133
134 // Pro preview banner (shown via JS when free user toggles Advanced Controls)
135 if (!defined('WPVR_PRO_VERSION')) {
136 add_action('admin_notices', 'wpvr_render_pro_preview_banner', 11);
137 }
138
139 }
140 run_wpvr();
141
142 /**
143 * Render Pro preview banner as admin notice.
144 * Hidden by default; shown via JS when free user toggles Advanced Controls.
145 *
146 * @since 8.5.44
147 */
148 function wpvr_render_pro_preview_banner() {
149 $screen = get_current_screen();
150 if (!$screen || $screen->id !== 'wpvr_item') {
151 return;
152 }
153 ?>
154 <div class="wpvr-pro-preview-banner" id="wpvr-pro-preview-banner" style="display:none;">
155 <div class="wpvr-pro-preview-banner__left">
156 <span class="wpvr-pro-preview-banner__icon">
157 <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor" stroke="none"><path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zm0 12.5c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/></svg>
158 </span>
159 <span class="wpvr-pro-preview-banner__text"><?php esc_html_e('You are previewing Pro features in action.', 'wpvr'); ?></span>
160 </div>
161 <div class="wpvr-pro-preview-banner__pricing">
162 <div class="wpvr-pro-preview-banner__pricing-top">
163 <span class="wpvr-pro-preview-banner__old-price"><?php esc_html_e('Normally $99.99/year', 'wpvr'); ?></span>
164 <span class="wpvr-pro-preview-banner__badge"><?php esc_html_e('SAVE 20%', 'wpvr'); ?></span>
165 </div>
166 <span class="wpvr-pro-preview-banner__new-price"><?php esc_html_e('Starting at $79.99/year', 'wpvr'); ?></span>
167 </div>
168 <a href="<?php echo esc_url('https://rextheme.com/wpvr/wpvr-pricing/?utm_source=plugin&utm_medium=pro-preview-banner&utm_campaign=advanced-controls'); ?>" class="wpvr-pro-preview-banner__btn" target="_blank" rel="noopener noreferrer">
169 <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" stroke="none"><path d="M5 16L3 5l5.5 5L12 4l3.5 6L21 5l-2 11H5zm0 2h14v1c0 .55-.45 1-1 1H6c-.55 0-1-.45-1-1v-1z"/></svg>
170 <?php esc_html_e('Upgrade to save changes', 'wpvr'); ?>
171 </a>
172 </div>
173 <?php
174 }
175
176
177 /**
178 * Array information checker
179 *
180 * @param mixed $needle
181 * @param mixed $haystack
182 * @param bool $strict
183 *
184 * @return bool
185 * @since 7.3.6
186 */
187 function wpvr_in_array_r($needle, $haystack, $strict = false)
188 {
189 foreach ($haystack as $item) {
190 if ((($strict ? $item === $needle : $item == $needle)) || is_array($item) && wpvr_in_array_r($needle, $item, $strict)) {
191 return true;
192 }
193 }
194 return false;
195 }
196
197 if ( ! function_exists( 'wpvr_parse_custom_control' ) ) {
198 /**
199 * Normalize custom control data into an array.
200 *
201 * @param mixed $custom_control Raw custom control value from meta.
202 *
203 * @return array
204 * @since 8.5.63
205 */
206 function wpvr_parse_custom_control( $custom_control ) {
207 if ( is_array( $custom_control ) ) {
208 return $custom_control;
209 }
210
211 if ( is_object( $custom_control ) ) {
212 return (array) $custom_control;
213 }
214
215 if ( ! is_string( $custom_control ) || '' === trim( $custom_control ) ) {
216 return array();
217 }
218
219 $unserialized = maybe_unserialize( $custom_control );
220 if ( is_array( $unserialized ) ) {
221 return $unserialized;
222 }
223
224 $decoded = json_decode( wp_unslash( $custom_control ), true );
225 if ( is_array( $decoded ) ) {
226 return $decoded;
227 }
228
229 return array();
230 }
231 }
232
233 if ( ! function_exists( 'wpvr_restore_modern_layout_default_controls' ) ) {
234 /**
235 * Restore Pannellum's default controls after WP VR Pro adds Modern layout CSS.
236 *
237 * The Modern layout stylesheet is added by WP VR Pro at filter priority 10.
238 * Running at priority 20 keeps this compatibility rule in the Free plugin while
239 * ensuring it is output after the Pro stylesheet, regardless of plugin load order.
240 *
241 * @param string $html Generated tour HTML.
242 * @param array $postdata Saved tour settings.
243 * @param int $id Tour post ID.
244 *
245 * @return string
246 * @since 9.0.0
247 */
248 function wpvr_restore_modern_layout_default_controls( $html, $postdata, $id ) {
249 $tour_layout = is_array( $postdata['tourLayout'] ?? null )
250 ? ( $postdata['tourLayout']['layout'] ?? 'default' )
251 : ( $postdata['tourLayout'] ?? 'default' );
252
253 if (
254 ! defined( 'WPVR_PRO_VERSION' )
255 || 'layout1' !== $tour_layout
256 || empty( $postdata['showControls'] )
257 ) {
258 return $html;
259 }
260
261 $custom_control = wp_parse_args(
262 wpvr_parse_custom_control( $postdata['customcontrol'] ?? array() ),
263 array(
264 'panupSwitch' => 'off',
265 'panDownSwitch' => 'off',
266 'panLeftSwitch' => 'off',
267 'panRightSwitch' => 'off',
268 'panZoomInSwitch' => 'off',
269 'panZoomOutSwitch' => 'off',
270 'panFullscreenSwitch' => 'off',
271 'gyroscopeSwitch' => 'off',
272 'backToHomeSwitch' => 'off',
273 )
274 );
275
276 $gyro_enabled = isset( $postdata['gyro'] )
277 && in_array( $postdata['gyro'], array( true, 1, '1', 'on' ), true );
278 $gyro_button_enabled = function_exists( 'wpvr_isMobileDevice' )
279 && wpvr_isMobileDevice()
280 && $gyro_enabled
281 && 'on' === $custom_control['gyroscopeSwitch'];
282
283 $custom_navigation_enabled = $gyro_button_enabled;
284 foreach (
285 array(
286 'panupSwitch',
287 'panDownSwitch',
288 'panLeftSwitch',
289 'panRightSwitch',
290 'panZoomInSwitch',
291 'panZoomOutSwitch',
292 'panFullscreenSwitch',
293 'backToHomeSwitch',
294 ) as $switch
295 ) {
296 if ( 'on' === $custom_control[ $switch ] ) {
297 $custom_navigation_enabled = true;
298 break;
299 }
300 }
301
302 if ( $custom_navigation_enabled ) {
303 return $html;
304 }
305
306 $pano_id = 'pano' . absint( $id );
307 $sprites_url = esc_url( WPVR_PLUGIN_PUBLIC_DIR_URL . 'lib/pannellum/src/css/img/sprites.svg' );
308
309 $html .= '<style id="wpvr-modern-default-controls-' . esc_attr( $id ) . '">
310 #' . esc_attr( $pano_id ) . ' .pnlm-controls-container .pnlm-zoom-controls.pnlm-controls {
311 display: block !important;
312 width: 28px !important;
313 height: 52px !important;
314 right: auto !important;
315 bottom: auto !important;
316 margin-top: 4px !important;
317 border: 1px solid rgba(0, 0, 0, 0.4) !important;
318 border-radius: 3px !important;
319 background-color: #fff !important;
320 background-image: none !important;
321 background-position: 0 0 !important;
322 background-repeat: repeat !important;
323 background-size: auto !important;
324 }
325
326 #' . esc_attr( $pano_id ) . ' .pnlm-controls-container .pnlm-fullscreen-toggle-button {
327 display: block !important;
328 width: 28px !important;
329 height: 26px !important;
330 right: auto !important;
331 bottom: auto !important;
332 margin-top: 4px !important;
333 border: 1px solid rgba(0, 0, 0, 0.4) !important;
334 border-radius: 3px !important;
335 background-color: #fff !important;
336 background-image: url("' . $sprites_url . '") !important;
337 background-repeat: repeat !important;
338 background-size: auto !important;
339 }
340
341 #' . esc_attr( $pano_id ) . ' .pnlm-controls-container .pnlm-fullscreen-toggle-button-inactive {
342 background-position: 1px -52px !important;
343 }
344
345 #et-boc .et-l #' . esc_attr( $pano_id ) . ' .pnlm-controls-container .pnlm-fullscreen-toggle-button-inactive {
346 background-position: -1px -53px !important;
347 }
348
349 #' . esc_attr( $pano_id ) . ' .pnlm-controls-container .pnlm-fullscreen-toggle-button-active {
350 background-position: 0 -78px !important;
351 }
352
353 #' . esc_attr( $pano_id ) . ' .pnlm-controls-container .pnlm-control:hover {
354 background-color: #f8f8f8 !important;
355 }
356 </style>';
357
358 return $html;
359 }
360 }
361
362 add_filter( 'wpvr_generate_tour_layout_html', 'wpvr_restore_modern_layout_default_controls', 20, 3 );
363
364 if ( ! function_exists( 'wpvr_render_scene_info_row' ) ) {
365 /**
366 * Keep scene title and author metadata aligned for every tour layout.
367 *
368 * WP VR Pro can replace the Pannellum asset registered by the Free plugin and
369 * adds Modern layout CSS at priority 10. This late, tour-scoped layer keeps
370 * both Pannellum copies and both layouts consistent without modifying Pro.
371 *
372 * @param string $html Generated tour HTML.
373 * @param array $postdata Saved tour settings.
374 * @param int $id Tour post ID.
375 *
376 * @return string
377 * @since 9.0.0
378 */
379 function wpvr_render_scene_info_row( $html, $postdata, $id ) {
380 if ( preg_match( '/id=[\'"](pano' . absint( $id ) . '(?:_\d+)?)[\'"]/', $html, $m ) ) {
381 $pano_id = $m[1];
382 } else {
383 $pano_id = 'pano' . absint( $id );
384 }
385 $pano_id_json = wp_json_encode( $pano_id );
386 $by_label = wp_json_encode( __( 'By', 'wpvr' ) );
387 $tour_layout = is_array( $postdata['tourLayout'] ?? null )
388 ? ( $postdata['tourLayout']['layout'] ?? 'default' )
389 : ( $postdata['tourLayout'] ?? 'default' );
390 $is_classic_layout = wp_json_encode( 'layout1' !== $tour_layout );
391
392 $html .= '<style id="wpvr-scene-info-' . esc_attr( $pano_id ) . '">
393 #' . esc_attr( $pano_id ) . ' .pnlm-panorama-info.wpvr-scene-info-row {
394 display: flex !important;
395 align-items: center !important;
396 justify-content: center !important;
397 gap: 10px !important;
398 box-sizing: border-box !important;
399 min-width: 0 !important;
400 overflow: hidden !important;
401 text-align: center !important;
402 z-index: 2147483647 !important;
403 -webkit-transform: translateZ(10000px) !important;
404 transform: translateZ(10000px) !important;
405 }
406
407 #' . esc_attr( $pano_id ) . ' .pnlm-panorama-info.wpvr-scene-info-row .pnlm-title-box {
408 flex: 0 1 auto !important;
409 min-width: 0 !important;
410 max-width: calc(70% - 5px) !important;
411 width: auto !important;
412 margin: 0 !important;
413 padding: 0 !important;
414 display: block !important;
415 overflow: hidden !important;
416 text-overflow: ellipsis !important;
417 white-space: nowrap !important;
418 }
419
420 #' . esc_attr( $pano_id ) . ' .pnlm-panorama-info.wpvr-scene-info-row .pnlm-author-box {
421 flex: 0 1 auto !important;
422 min-width: 0 !important;
423 max-width: calc(30% - 5px) !important;
424 margin: 0 !important;
425 border-left: 1px solid rgba(255, 255, 255, 0.45) !important;
426 padding: 0 0 0 10px !important;
427 display: block !important;
428 overflow: hidden !important;
429 line-height: 1.2 !important;
430 text-overflow: ellipsis !important;
431 white-space: nowrap !important;
432 }
433
434 #' . esc_attr( $pano_id ) . ' .pnlm-panorama-info .wpvr-author-label {
435 flex: none !important;
436 margin-right: 4px !important;
437 font-size: 10px !important;
438 font-weight: 700 !important;
439 letter-spacing: 0.06em !important;
440 opacity: 0.8 !important;
441 text-transform: uppercase !important;
442 }
443
444 #' . esc_attr( $pano_id ) . ' .pnlm-panorama-info .pnlm-author-box a {
445 color: inherit !important;
446 text-decoration: underline !important;
447 text-underline-offset: 2px !important;
448 }
449
450 #' . esc_attr( $pano_id ) . ' .pnlm-panorama-info.wpvr-has-title:not(.wpvr-has-author) .pnlm-title-box,
451 #' . esc_attr( $pano_id ) . ' .pnlm-panorama-info.wpvr-has-author:not(.wpvr-has-title) .pnlm-author-box {
452 max-width: 100% !important;
453 border-left: 0 !important;
454 padding-left: 0 !important;
455 }
456
457 #' . esc_attr( $pano_id ) . '.wpvr-layout-classic.wpvr-has-scene-info .scene-gallery.wpvr-gallery--classic {
458 bottom: calc(var(--wpvr-scene-info-height, 34px) + 8px) !important;
459 }
460
461 #' . esc_attr( $pano_id ) . '.wpvr-layout-classic.wpvr-has-scene-info .vrgcontrols {
462 bottom: calc(var(--wpvr-scene-info-height, 34px) + 4px) !important;
463 }
464
465 #' . esc_attr( $pano_id ) . '.wpvr-layout-classic.wpvr-has-scene-info .wpvr_slider_nav {
466 bottom: calc(var(--wpvr-scene-info-height, 34px) + 15px) !important;
467 }
468
469 #' . esc_attr( $pano_id ) . '.wpvr-layout-classic.wpvr-has-scene-info .controls {
470 bottom: calc(var(--wpvr-scene-info-height, 34px) + 6px) !important;
471 }
472
473 @media (max-width: 767px) {
474 #' . esc_attr( $pano_id ) . ' .pnlm-panorama-info.wpvr-scene-info-row {
475 gap: 7px !important;
476 padding-right: 10px !important;
477 padding-left: 10px !important;
478 }
479
480 #' . esc_attr( $pano_id ) . ' .pnlm-panorama-info.wpvr-scene-info-row .pnlm-title-box {
481 max-width: calc(65% - 4px) !important;
482 }
483
484 #' . esc_attr( $pano_id ) . ' .pnlm-panorama-info.wpvr-scene-info-row .pnlm-author-box {
485 max-width: calc(35% - 4px) !important;
486 padding-left: 7px !important;
487 }
488 }
489 </style>
490 <script id="wpvr-scene-info-script-' . esc_attr( $pano_id ) . '">
491 (function () {
492 var pano = document.getElementById(' . $pano_id_json . ');
493 var byLabel = ' . $by_label . ';
494
495 if (!pano || !window.MutationObserver) {
496 return;
497 }
498
499 function addAuthorLabel(author) {
500 if (!author || !author.textContent.trim() || author.querySelector(".wpvr-author-label")) {
501 return;
502 }
503
504 var firstNode = author.firstChild;
505 if (firstNode ? firstNode.nodeType === 3 : false) {
506 var prefix = byLabel + " ";
507 if (firstNode.nodeValue.indexOf(prefix) === 0) {
508 firstNode.nodeValue = firstNode.nodeValue.slice(prefix.length);
509 }
510 }
511
512 var label = document.createElement("span");
513 label.className = "wpvr-author-label";
514 label.textContent = byLabel;
515 author.insertBefore(label, author.firstChild);
516 }
517
518 function syncSceneInfo() {
519 var info = pano.querySelector(".pnlm-panorama-info");
520 if (!info) {
521 return;
522 }
523
524 // Keep the Pannellum full-screen drag layer in its original
525 // stacking order so hotspots remain clickable. Promote only
526 // the compact metadata bar to the tour root instead.
527 if (info.parentNode !== pano) {
528 pano.appendChild(info);
529 }
530
531 var title = info.querySelector(".pnlm-title-box");
532 var author = info.querySelector(".pnlm-author-box");
533 var hasTitle = Boolean(title ? title.textContent.trim() : false);
534 var hasAuthor = Boolean(author ? author.textContent.trim() : false);
535
536 if (hasTitle) {
537 title.setAttribute("title", title.textContent.trim());
538 } else if (title) {
539 title.removeAttribute("title");
540 }
541
542 if (hasAuthor) {
543 addAuthorLabel(author);
544 var authorName = author.textContent.replace(byLabel, "").trim();
545 author.setAttribute("aria-label", byLabel + " " + authorName);
546 author.setAttribute("title", authorName);
547 } else if (author) {
548 author.removeAttribute("aria-label");
549 author.removeAttribute("title");
550 }
551
552 info.classList.toggle("wpvr-scene-info-row", hasTitle || hasAuthor);
553 info.classList.toggle("wpvr-has-title", hasTitle);
554 info.classList.toggle("wpvr-has-author", hasAuthor);
555 info.style.display = hasTitle || hasAuthor ? "flex" : "none";
556
557 var hasSceneInfo = hasTitle || hasAuthor;
558 var isClassicLayout = ' . $is_classic_layout . ';
559 pano.classList.toggle("wpvr-has-scene-info", hasSceneInfo);
560 pano.classList.toggle("wpvr-layout-classic", isClassicLayout);
561
562 if (hasSceneInfo) {
563 pano.style.setProperty("--wpvr-scene-info-height", info.offsetHeight + "px");
564 } else {
565 pano.style.removeProperty("--wpvr-scene-info-height");
566 }
567 }
568
569 var observer = new MutationObserver(syncSceneInfo);
570 observer.observe(pano, { childList: true, subtree: true, characterData: true });
571 syncSceneInfo();
572 }());
573 </script>';
574
575 return $html;
576 }
577 }
578
579 add_filter( 'wpvr_generate_tour_layout_html', 'wpvr_render_scene_info_row', 30, 3 );
580
581 if ( ! function_exists( 'wpvr_text_overlay_hex_to_rgba' ) ) {
582 /**
583 * Convert HEX color and opacity percentage to RGBA CSS string.
584 *
585 * Backward-compatibility wrapper for RexTheme\WPVR\Frontend\TextOverlayRenderer::hex_to_rgba().
586 *
587 * @param string $hex Hex color code.
588 * @param float|int $opacity_percent Opacity 0-100.
589 * @return string
590 */
591 function wpvr_text_overlay_hex_to_rgba( $hex, $opacity_percent = 55 ) {
592 if ( class_exists( 'RexTheme\WPVR\Frontend\TextOverlayRenderer' ) ) {
593 return \RexTheme\WPVR\Frontend\TextOverlayRenderer::hex_to_rgba( $hex, $opacity_percent );
594 }
595 return 'rgba(40, 30, 25, 0.55)';
596 }
597 }
598
599 if ( ! function_exists( 'wpvr_render_frontend_text_overlays' ) ) {
600 /**
601 * Render the text overlay layer(s) on the frontend for each scene in the tour.
602 *
603 * Backward-compatibility wrapper for RexTheme\WPVR\Frontend\TextOverlayRenderer::render().
604 *
605 * @param string $html Generated tour HTML.
606 * @param array $postdata Saved tour settings.
607 * @param int $id Tour post ID.
608 *
609 * @return string
610 */
611 function wpvr_render_frontend_text_overlays( $html, $postdata, $id ) {
612 if ( class_exists( 'RexTheme\WPVR\Frontend\TextOverlayRenderer' ) ) {
613 return \RexTheme\WPVR\Frontend\TextOverlayRenderer::render( $html, $postdata, $id );
614 }
615 return $html;
616 }
617 }
618
619 if ( class_exists( 'RexTheme\WPVR\Frontend\TextOverlayRenderer' ) ) {
620 \RexTheme\WPVR\Frontend\TextOverlayRenderer::init();
621 }
622
623 if ( ! function_exists( 'wpvr_render_explainer_button' ) ) {
624 /**
625 * Render the WPVR explainer button independently.
626 *
627 * @param array|null $custom_control
628 * @param array $postdata
629 * @param bool $is_pro
630 * @param bool $autoload
631 * @param string $explainer_right
632 * @param int|string $id
633 *
634 * @return string
635 * @since 8.5.63
636 */
637 function wpvr_render_explainer_button( $custom_control, $postdata, $is_pro, $autoload, $explainer_right, $id ) {
638 $html = '';
639 $explainer_enabled = isset( $postdata['explainerSwitch'] )
640 && in_array( $postdata['explainerSwitch'], array( true, 1, '1', 'on' ), true );
641 $explainer_icon = 'fa fa-video';
642 $explainer_color = '#f7fffb';
643
644 if ( isset( $custom_control ) ) {
645 $explainer_icon = isset( $custom_control['explainerIcon'] ) ? $custom_control['explainerIcon'] : $explainer_icon;
646 $explainer_color = isset( $custom_control['explainerColor'] ) ? $custom_control['explainerColor'] : $explainer_color;
647 } elseif ( isset( $postdata['customcontrol'] ) ) {
648 $raw_control = wpvr_parse_custom_control( $postdata['customcontrol'] );
649 $explainer_icon = isset( $raw_control['explainerIcon'] ) ? $raw_control['explainerIcon'] : $explainer_icon;
650 $explainer_color = isset( $raw_control['explainerColor'] ) ? $raw_control['explainerColor'] : $explainer_color;
651 }
652
653 $pro_license_status = get_option( 'wpvr_edd_license_status' );
654 if ( $explainer_enabled && $pro_license_status === 'valid' && $is_pro ) {
655 $explainer_style = empty( $postdata['explainerContent'] )
656 ? 'pointer-events: none; opacity: 0.5;'
657 : '';
658
659 // Initial display:none to prevent flash for non-autoload tours.
660 $initial_display = ! $autoload ? 'display:none; ' : '';
661
662 $html .= '<div class="explainer_button" id="explainer_button_' . esc_attr( $id ) . '" style="' . $initial_display . 'right:' . esc_attr( $explainer_right ) . '; ' . esc_attr( $explainer_style ) . '">';
663 $html .= '<div class="ctrl" id="explainer_target_' . esc_attr( $id ) . '"><i class="' . esc_attr( $explainer_icon ) . '" style="color:' . esc_attr( $explainer_color ) . ';"></i></div>';
664 $html .= '</div>';
665 }
666
667 return $html;
668 }
669 }
670
671 // Linno telemetry integration
672 function wpvr_block()
673 {
674 wp_register_script(
675 'wpvr-block',
676 plugins_url('build/index.build.js', __FILE__),
677 array('wp-blocks', 'wp-element', 'wp-components', 'wp-editor')
678 );
679
680 if (is_admin()) {
681 wp_enqueue_style(
682 'gutyblocks/guty-block',
683 plugins_url('src/view.css', __FILE__),
684 array()
685 );
686 }
687
688 if (function_exists('register_block_type')) {
689 register_block_type('wpvr/wpvr-block', array(
690 'attributes' => array(
691 'id' => array(
692 'type' => 'string',
693 'default' => '0',
694 ),
695 'width' => array(
696 'type' => 'string',
697 'default' => '600',
698 ),
699 'width_unit' => array(
700 'type' => 'string',
701 'default' => 'px',
702 ),
703 'height' => array(
704 'type' => 'string',
705 'default' => '400',
706 ),
707 'height_unit' => array(
708 'type' => 'string',
709 'default' => 'px',
710 ),
711 'mobile_height' => array(
712 'type' => 'string',
713 'default' => '300',
714 ),
715 'mobile_height_unit' => array(
716 'type' => 'string',
717 'default' => 'px',
718 ),
719 'radius' => array(
720 'type' => 'string',
721 'default' => '0',
722 ),
723 'radius_unit' => array(
724 'type' => 'string',
725 'default' => 'px',
726 ),
727 'border_width' => array(
728 'type' => 'string',
729 'default' => '0',
730 ),
731 'border_style' => array(
732 'type' => 'string',
733 'default' => 'none',
734 ),
735 'border_color' => array(
736 'type' => 'string',
737 'default' => '',
738 ),
739 ),
740 'editor_script' => 'wpvr-block',
741 'render_callback' => 'wpvr_block_render',
742 ));
743 }
744 }
745
746 add_action('init', 'wpvr_block');
747
748 function wpvr_block_render($attributes)
749 {
750 $id = isset($attributes['id']) ? (int) $attributes['id'] : 0;
751 if (!$id) {
752 return '<div class="wpvr-no-tour-selected">' . esc_html__('Please select a tour from the block settings panel.', 'wpvr') . '</div>';
753 }
754
755 $width = isset($attributes['width']) ? $attributes['width'] : '600';
756 if ('fullwidth' === $width) {
757 $width_str = 'fullwidth';
758 } else {
759 $width_unit = isset($attributes['width_unit']) ? $attributes['width_unit'] : 'px';
760 if (preg_match('/(px|%|vw|vh)$/', (string) $width)) {
761 $width_str = $width;
762 } else {
763 $width_str = $width . $width_unit;
764 }
765 }
766
767 $height = isset($attributes['height']) ? $attributes['height'] : '400';
768 $height_unit = isset($attributes['height_unit']) ? $attributes['height_unit'] : 'px';
769 if (!preg_match('/(px|%|vw|vh)$/', (string) $height)) {
770 $height .= $height_unit;
771 }
772
773 $mobile_height = isset($attributes['mobile_height']) ? $attributes['mobile_height'] : '300';
774 $mobile_height_unit = isset($attributes['mobile_height_unit']) ? $attributes['mobile_height_unit'] : 'px';
775 if (!preg_match('/(px|%|vw|vh)$/', (string) $mobile_height)) {
776 $mobile_height .= $mobile_height_unit;
777 }
778
779 $radius = isset($attributes['radius']) ? $attributes['radius'] : '0';
780 $radius_unit = isset($attributes['radius_unit']) ? $attributes['radius_unit'] : 'px';
781 if (!preg_match('/(px|%|vw|vh)$/', (string) $radius)) {
782 $radius .= $radius_unit;
783 }
784
785 $shortcode = sprintf(
786 '[wpvr id="%d" width="%s" height="%s" mobile_height="%s" radius="%s"]',
787 $id,
788 esc_attr($width_str),
789 esc_attr($height),
790 esc_attr($mobile_height),
791 esc_attr($radius)
792 );
793
794 $output = do_shortcode($shortcode);
795
796 // Extract any <script> tags so they are not corrupted by wptexturize (which runs at priority 10 on the_content, after do_blocks at priority 9).
797 $scripts = '';
798 if (preg_match_all('/<script\b[^>]*>[\s\S]*?<\/script>/i', $output, $matches)) {
799 $scripts = implode("\n", $matches[0]);
800 $output = preg_replace('/<script\b[^>]*>[\s\S]*?<\/script>/i', '', $output);
801 }
802
803 if (!empty($scripts)) {
804 if (wp_doing_ajax() || (defined('REST_REQUEST') && REST_REQUEST)) {
805 $output .= "\n" . $scripts;
806 } else {
807 add_action('wp_footer', function() use ($scripts) {
808 echo $scripts; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
809 }, 20);
810 }
811 }
812
813 $border_css = '';
814 if (!empty($attributes['border_width']) && !empty($attributes['border_style']) && 'none' !== $attributes['border_style']) {
815 $border_width = rtrim((string) $attributes['border_width'], 'px') . 'px';
816 $border_color = !empty($attributes['border_color']) ? $attributes['border_color'] : 'transparent';
817 $border_css = 'border: ' . esc_attr($border_width . ' ' . $attributes['border_style'] . ' ' . $border_color) . ';';
818 }
819
820 $class_name = !empty($attributes['className']) ? ' ' . esc_attr($attributes['className']) : '';
821
822 if ('' !== $border_css || '' !== $class_name) {
823 $wrapper_style = $border_css ? ' style="' . $border_css . ' display: inline-block; width: 100%; max-width: ' . esc_attr('fullwidth' === $width_str ? '100%' : $width_str) . ';"' : '';
824 return '<div class="wpvr-block-wrapper' . $class_name . '"' . $wrapper_style . '>' . $output . '</div>';
825 }
826
827 return $output;
828 }
829
830 function sanitize_content_preserve_styles($content, $allow_forms = false) {
831 // Decode HTML entities first (in case content was encoded in database)
832 $content = html_entity_decode($content, ENT_QUOTES | ENT_HTML5, 'UTF-8');
833
834 // Escape or strip <script> blocks
835 if ($allow_forms) {
836 $content = preg_replace('/<script\b[^>]*>[\s\S]*?<\/script>/i', '', $content);
837 } else {
838 $content = preg_replace_callback('/<script\b[^>]*>(.*?)<\/script>/si', function($matches) {
839 return esc_html($matches[0]); // Convert to plain text
840 }, $content);
841 }
842
843 // Strip dangerous URL-based attributes
844 $content = preg_replace('/(href|action|formaction)\s*=\s*["\']?\s*(javascript|vbscript|data|about):/i', '$1=""', $content);
845
846 // Escape inline event handlers (onclick, onhover, etc.) to display as text
847 $content = preg_replace_callback('/\s*(on\w+)\s*=\s*(["\'])([^"\']*)\2/i', function($matches) {
848 // Escape the attribute value but keep the attribute name visible as text
849 return ' ' . esc_html($matches[1]) . '=' . $matches[2] . esc_html($matches[3]) . $matches[2];
850 }, $content);
851 $content = preg_replace_callback('/\s*(on\w+)\s*=\s*([^>\s]+)/i', function($matches) {
852 // Handle unquoted event handlers
853 return ' ' . esc_html($matches[1]) . '=' . esc_html($matches[2]);
854 }, $content);
855
856 // Remove unsafe embedded/interactive elements
857 if ($allow_forms) {
858 $content = preg_replace('/<(object|embed|applet|frame|frameset|meta|link|base)\b[^>]*>/i', '', $content);
859 $content = preg_replace('/<\/(object|embed|applet|frame|frameset|meta|link|base)>/i', '', $content);
860 } else {
861 $content = preg_replace('/<(object|embed|applet|frame|frameset|meta|link|base|form|input|button|textarea|select|option)\b[^>]*>/i', '', $content);
862 $content = preg_replace('/<\/(object|embed|applet|frame|frameset|meta|link|base|form|input|button|textarea|select|option)>/i', '', $content);
863 }
864
865 // Clean style attributes safely
866 $content = preg_replace_callback('/style\s*=\s*["\']([^"\']*)["\']/', function($matches) {
867 $style = $matches[1];
868 $style = preg_replace('/expression\s*\(/i', '', $style);
869 $style = preg_replace('/(javascript|vbscript|data|about)\s*:/i', '', $style);
870 $style = preg_replace('/url\s*\(\s*["\']?\s*(javascript|vbscript|data):/i', '', $style);
871 $style = preg_replace('/behavior\s*:/i', '', $style);
872 $style = preg_replace('/-moz-binding\s*:/i', '', $style);
873 return 'style="' . esc_attr($style) . '"';
874 }, $content);
875
876 // Sanitize <style> blocks
877 $content = preg_replace_callback('/<style\b[^>]*>(.*?)<\/style>/si', function($matches) {
878 $css = $matches[1];
879 $css = preg_replace('/(expression|javascript|vbscript|data|about)\s*:/i', '', $css);
880 $css = preg_replace('/url\s*\(\s*["\']?\s*(javascript|vbscript|data):/i', '', $css);
881 $css = preg_replace('/behavior\s*:/i', '', $css);
882 $css = preg_replace('/-moz-binding\s*:/i', '', $css);
883 return '<style>' . esc_html($css) . '</style>';
884 }, $content);
885
886 // Allow iframes and styles from safe sources only
887 $allowed_tags = wp_kses_allowed_html('post');
888 $allowed_tags['style'] = [
889 'type' => true,
890 'id' => true,
891 'class' => true,
892 'media' => true,
893 ];
894 $allowed_tags['iframe'] = [
895 'src' => true,
896 'width' => true,
897 'height' => true,
898 'frameborder' => true,
899 'allowfullscreen' => true,
900 'class' => true,
901 'style' => true,
902 'title' => true,
903 'allow' => true,
904 'name' => true,
905 'referrerpolicy' => true,
906 'loading' => true,
907 'sandbox' => true,
908 ];
909 $allowed_tags['img'] = [
910 'src' => true,
911 'alt' => true,
912 'title' => true,
913 'width' => true,
914 'height' => true,
915 'class' => true,
916 'id' => true,
917 'style' => true,
918 'loading' => true,
919 'srcset' => true,
920 'sizes' => true,
921 ];
922
923 if ($allow_forms) {
924 $form_attributes = [
925 'id' => true,
926 'class' => true,
927 'style' => true,
928 'name' => true,
929 'value' => true,
930 'type' => true,
931 'placeholder' => true,
932 'action' => true,
933 'method' => true,
934 'target' => true,
935 'enctype' => true,
936 'disabled' => true,
937 'readonly' => true,
938 'required' => true,
939 'checked' => true,
940 'selected' => true,
941 'multiple' => true,
942 'size' => true,
943 'rows' => true,
944 'cols' => true,
945 'maxlength' => true,
946 'minlength' => true,
947 'min' => true,
948 'max' => true,
949 'step' => true,
950 'pattern' => true,
951 'autocomplete' => true,
952 'autofocus' => true,
953 'for' => true,
954 'data-*' => true,
955 'data-form_id' => true,
956 'data-form_instance' => true,
957 'data-name' => true,
958 'data-type' => true,
959 'aria-invalid' => true,
960 'aria-required' => true,
961 'aria-label' => true,
962 'aria-describedby' => true,
963 'aria-labelledby' => true,
964 ];
965 $allowed_tags['form'] = $form_attributes;
966 $allowed_tags['input'] = $form_attributes;
967 $allowed_tags['button'] = $form_attributes;
968 $allowed_tags['textarea'] = $form_attributes;
969 $allowed_tags['select'] = $form_attributes;
970 $allowed_tags['option'] = $form_attributes;
971 $allowed_tags['optgroup'] = $form_attributes;
972 $allowed_tags['label'] = $form_attributes;
973 $allowed_tags['fieldset'] = $form_attributes;
974 $allowed_tags['legend'] = $form_attributes;
975 if (!isset($allowed_tags['div'])) {
976 $allowed_tags['div'] = [];
977 }
978 $allowed_tags['div']['data-*'] = true;
979 $allowed_tags['div']['data-form_id'] = true;
980 $allowed_tags['div']['data-form_instance'] = true;
981 if (!isset($allowed_tags['span'])) {
982 $allowed_tags['span'] = [];
983 }
984 $allowed_tags['span']['data-*'] = true;
985 }
986
987 // Apply wp_kses() to keep only allowed tags/attributes
988 $content = wp_kses($content, $allowed_tags);
989
990 // Finally, validate iframe src for security (allow https only, block javascript: etc.)
991 $content = preg_replace_callback('/<iframe[^>]+src=["\']([^"\']+)["\'][^>]*>(?:<\/iframe>)?/i', function($matches) {
992 $src = $matches[1];
993 // Allow any https:// or http:// URL, but block javascript:, data:, vbscript:, etc.
994 if (preg_match('/^(https?:)?\/\//i', $src) && !preg_match('/^(javascript|data|vbscript|about):/i', $src)) {
995 return $matches[0]; // keep safe iframe
996 }
997 // Strip unsafe iframe
998 return '';
999 }, $content);
1000
1001 return $content;
1002 }
1003 function wpvr_hex2rgb($colour)
1004 {
1005 if (isset($colour[0]) && $colour[0] == '#') {
1006 $colour = substr($colour, 1);
1007 }
1008 if (strlen($colour) == 6) {
1009 list($r, $g, $b) = array($colour[0] . $colour[1], $colour[2] . $colour[3], $colour[4] . $colour[5]);
1010 } elseif (strlen($colour) == 3) {
1011 list($r, $g, $b) = array($colour[0] . $colour[0], $colour[1] . $colour[1], $colour[2] . $colour[2]);
1012 } else {
1013 return false;
1014 }
1015 $r = hexdec($r);
1016 $g = hexdec($g);
1017 $b = hexdec($b);
1018 return array($r . ', ' . $g . ', ' . $b);
1019 }
1020
1021 function wpvr_HTMLToRGB($htmlCode)
1022 {
1023 $r = 0;
1024 $g = 0;
1025 $b = 0;
1026 if (isset($htmlCode[0]) && $htmlCode[0] == '#') {
1027 $htmlCode = substr($htmlCode, 1);
1028 }
1029
1030 if (strlen($htmlCode) == 3) {
1031 $htmlCode = $htmlCode[0] . $htmlCode[0] . $htmlCode[1] . $htmlCode[1] . $htmlCode[2] . $htmlCode[2];
1032 }
1033
1034 if (isset($htmlCode[0]) && isset($htmlCode[1])) {
1035 $r = hexdec($htmlCode[0] . $htmlCode[1]);
1036 }
1037 if (isset($htmlCode[2]) && isset($htmlCode[3])) {
1038 $g = hexdec($htmlCode[2] . $htmlCode[3]);
1039 }
1040 if (isset($htmlCode[4]) && isset($htmlCode[5])) {
1041 $b = hexdec($htmlCode[4] . $htmlCode[5]);
1042 }
1043
1044 return $b + ($g << 0x8) + ($r << 0x10);
1045 }
1046
1047 function wpvr_RGBToHSL($RGB)
1048 {
1049 $r = 0xFF & ($RGB >> 0x10);
1050 $g = 0xFF & ($RGB >> 0x8);
1051 $b = 0xFF & $RGB;
1052
1053 $r = ((float)$r) / 255.0;
1054 $g = ((float)$g) / 255.0;
1055 $b = ((float)$b) / 255.0;
1056
1057 $maxC = max($r, $g, $b);
1058 $minC = min($r, $g, $b);
1059
1060 $l = ($maxC + $minC) / 2.0;
1061
1062 if ($maxC == $minC) {
1063 $s = 0;
1064 $h = 0;
1065 } else {
1066 if ($l < .5) {
1067 $s = ($maxC - $minC) / ($maxC + $minC);
1068 } else {
1069 $s = ($maxC - $minC) / (2.0 - $maxC - $minC);
1070 }
1071 if ($r == $maxC) {
1072 $h = ($g - $b) / ($maxC - $minC);
1073 }
1074 if ($g == $maxC) {
1075 $h = 2.0 + ($b - $r) / ($maxC - $minC);
1076 }
1077 if ($b == $maxC) {
1078 $h = 4.0 + ($r - $g) / ($maxC - $minC);
1079 }
1080
1081 $h = $h / 6.0;
1082 }
1083
1084 $h = (int)round(255.0 * $h);
1085 $s = (int)round(255.0 * $s);
1086 $l = (int)round(255.0 * $l);
1087
1088 return (object) array('hue' => $h, 'saturation' => $s, 'lightness' => $l);
1089 }
1090
1091 add_action('rest_api_init', 'wpvr_rest_data_route');
1092 function wpvr_rest_data_route()
1093 {
1094 register_rest_route('wpvr/v1', '/panodata/', array(
1095 'methods' => 'GET',
1096 'callback' => 'wpvr_rest_data_set',
1097 'permission_callback' => 'wpvr_rest_route_permission'
1098 ));
1099 }
1100
1101 function wpvr_rest_route_permission()
1102 {
1103 $post_type_obj = get_post_type_object( 'wpvr_item' );
1104 $edit_cap = $post_type_obj ? $post_type_obj->cap->edit_posts : 'edit_wpvr_tours';
1105 return current_user_can( $edit_cap ) || current_user_can( 'edit_posts' );
1106 }
1107
1108 function wpvr_rest_data_set()
1109 {
1110 $post_type_obj = get_post_type_object( 'wpvr_item' );
1111 $edit_cap = $post_type_obj ? $post_type_obj->cap->edit_posts : 'edit_wpvr_tours';
1112 $post_status = current_user_can( $edit_cap ) ? array( 'publish', 'draft', 'private' ) : 'publish';
1113
1114 $query = new WP_Query(array(
1115 'post_type' => 'wpvr_item',
1116 'post_status' => $post_status,
1117 'posts_per_page' => -1,
1118 ));
1119
1120 $wpvr_list = array();
1121 $list_none = array('value' => 0, 'label' => 'None');
1122 array_push($wpvr_list, $list_none);
1123 while ($query->have_posts()) {
1124 $query->the_post();
1125 $title = mb_convert_encoding(get_the_title(), 'UTF-8', 'HTML-ENTITIES');
1126 $post_id = get_the_ID();
1127 $title = $post_id . ' : ' . $title;
1128 $list_ob = array('value' => $post_id, 'label' => $title);
1129 array_push($wpvr_list, $list_ob);
1130 }
1131 wp_reset_postdata();
1132
1133 return $wpvr_list;
1134 }
1135
1136 function wpvr_isMobileDevice()
1137 {
1138 $user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '';
1139 return (bool) preg_match("/(android|avantgo|blackberry|bolt|boost|cricket|docomo|fone|hiptop|ipad|mini|mobi|palm|phone|pie|tablet|up\.browser|up\.link|webos|wos)/i", $user_agent);
1140 }
1141
1142 function wpvr_directory()
1143 {
1144 $upload = wp_upload_dir();
1145 $upload_dir = $upload['basedir'];
1146 $upload_dir_temp = $upload_dir . '/wpvr/temp/';
1147 if (!is_dir($upload_dir_temp)) {
1148 wp_mkdir_p($upload_dir_temp, 0700);
1149 }
1150 }
1151
1152 add_action('admin_init', 'wpvr_directory');
1153
1154
1155 function wpvr_add_role_cap()
1156 {
1157 $editor_active = get_option('wpvr_editor_active');
1158
1159 $author_active = get_option('wpvr_author_active');
1160
1161 $admin = get_role('administrator');
1162 $admin->add_cap('publish_wpvr_tour');
1163 $admin->add_cap('edit_wpvr_tours');
1164 $admin->add_cap('read_wpvr_tour');
1165 $admin->add_cap('edit_wpvr_tour');
1166 $admin->add_cap('edit_wpvr_tours');
1167 $admin->add_cap('publish_wpvr_tours');
1168 $admin->add_cap('publish_wpvr_tour');
1169 $admin->add_cap('delete_wpvr_tour');
1170 $admin->add_cap('edit_other_wpvr_tours');
1171 $admin->add_cap('delete_other_wpvr_tours');
1172
1173 if ($editor_active == "true") {
1174 $editor = get_role('editor');
1175 if ($editor) {
1176 $editor->add_cap('publish_wpvr_tour');
1177 $editor->add_cap('edit_wpvr_tours');
1178 $editor->add_cap('read_wpvr_tour');
1179 $editor->add_cap('edit_wpvr_tour');
1180 $editor->add_cap('edit_wpvr_tours');
1181 $editor->add_cap('publish_wpvr_tours');
1182 $editor->add_cap('publish_wpvr_tour');
1183 $editor->add_cap('delete_wpvr_tour');
1184 $editor->add_cap('edit_other_wpvr_tours');
1185 $editor->add_cap('delete_other_wpvr_tours');
1186 }
1187 } else {
1188 $editor = get_role('editor');
1189 if ($editor) {
1190 $editor->remove_cap('publish_wpvr_tour');
1191 $editor->remove_cap('edit_wpvr_tours');
1192 $editor->remove_cap('read_wpvr_tour');
1193 $editor->remove_cap('edit_wpvr_tour');
1194 $editor->remove_cap('edit_wpvr_tours');
1195 $editor->remove_cap('publish_wpvr_tours');
1196 $editor->remove_cap('publish_wpvr_tour');
1197 $editor->remove_cap('delete_wpvr_tour');
1198 $editor->remove_cap('edit_other_wpvr_tours');
1199 $editor->remove_cap('delete_other_wpvr_tours');
1200 }
1201 }
1202
1203 if ($author_active == "true") {
1204 $author = get_role('author');
1205 if ($author) {
1206 $author->add_cap('read_wpvr_tour');
1207 $author->add_cap('edit_wpvr_tour');
1208 $author->add_cap('edit_wpvr_tours');
1209 $author->add_cap('publish_wpvr_tours');
1210 $author->add_cap('publish_wpvr_tour');
1211 $author->add_cap('delete_wpvr_tour');
1212 }
1213 } else {
1214 $author = get_role('author');
1215 if ($author) {
1216 $author->remove_cap('read_wpvr_tour');
1217 $author->remove_cap('edit_wpvr_tour');
1218 $author->remove_cap('edit_wpvr_tours');
1219 $author->remove_cap('publish_wpvr_tours');
1220 $author->remove_cap('publish_wpvr_tour');
1221 $author->remove_cap('delete_wpvr_tour');
1222 }
1223 }
1224
1225 if(is_plugin_active( 'dokan-lite/dokan.php' ) || is_plugin_active( 'dokan-pro/dokan.php' )){
1226 $dokan_vendor_active = get_option('dokan_vendor_active');
1227
1228 if( 'true' === $dokan_vendor_active){
1229 $seller = get_role('seller');
1230 if ($seller) {
1231 $seller->add_cap('read_wpvr_tour');
1232 $seller->add_cap('edit_wpvr_tour');
1233 $seller->add_cap('edit_wpvr_tours');
1234 $seller->add_cap('publish_wpvr_tours');
1235 $seller->add_cap('publish_wpvr_tour');
1236 $seller->add_cap('delete_wpvr_tour');
1237 }
1238 } else{
1239 $seller = get_role('seller');
1240 if ($seller) {
1241 $seller->remove_cap('read_wpvr_tour');
1242 $seller->remove_cap('edit_wpvr_tour');
1243 $seller->remove_cap('edit_wpvr_tours');
1244 $seller->remove_cap('publish_wpvr_tours');
1245 $seller->remove_cap('publish_wpvr_tour');
1246 $seller->remove_cap('delete_wpvr_tour');
1247 }
1248 }
1249 }
1250
1251
1252 }
1253
1254 add_action('admin_init', 'wpvr_add_role_cap', 999);
1255
1256 function wpvr_role_management_from_post_type($args, $post_type)
1257 {
1258 if ('wpvr_item' !== $post_type) {
1259 return $args;
1260 }
1261
1262 $editor_active = get_option('wpvr_editor_active');
1263 $author_active = get_option('wpvr_author_active');
1264 $user = wp_get_current_user();
1265
1266 if ($editor_active == "true") {
1267 if (in_array('editor', (array) $user->roles)) {
1268 $args['show_in_menu'] = true;
1269 }
1270 }
1271
1272 if ($author_active == "true") {
1273 if (in_array('author', (array) $user->roles)) {
1274 $args['show_in_menu'] = true;
1275 }
1276 }
1277
1278 if(is_plugin_active( 'dokan-lite/dokan.php' ) || is_plugin_active( 'dokan-pro/dokan.php' )){
1279 $dokan_vendor_active = get_option('dokan_vendor_active');
1280 if( 'true' === $dokan_vendor_active){
1281 if (in_array('seller', (array) $user->roles)) {
1282 $args['show_in_menu'] = true;
1283 }
1284 }
1285
1286 }
1287
1288 return $args;
1289 }
1290 add_filter('register_post_type_args', 'wpvr_role_management_from_post_type', 10, 2);
1291
1292 function wpvr_cache_admin_notice()
1293 {
1294 $option = get_option('wpvr_warning');
1295 if (!$option) {
1296 ?>
1297 <div class="notice notice-warning" id="wpvr-warning" style="position: relative;">
1298 <p><?php _e('Since you have updated the plugin, please clear the browser cache for smooth functioning. Follow these steps if you are using <a href="https://support.google.com/accounts/answer/32050?co=GENIE.Platform%3DDesktop&hl=en" target="_blank">Google Chrome</a>, <a href="https://support.mozilla.org/en-US/kb/how-clear-firefox-cache" target="_blank">Mozilla Firefox</a>, <a href="https://clear-my-cache.com/en/apple-mac-os/safari.html" target="_blank">Safai</a> or <a href="https://support.microsoft.com/en-us/help/10607/microsoft-edge-view-delete-browser-history" target="_blank">Microsoft Edge</a>', 'wpvr'); ?></p>
1299 <button type="button" id="wpvr-dismissible" class="notice-dismiss"><span class="screen-reader-text">Dismiss this notice.</span></button>
1300 </div>
1301 <?php
1302 }
1303 }
1304 // add_action('admin_notices', 'wpvr_cache_admin_notice');
1305
1306 //===Oxygen widget===//
1307 add_action('plugins_loaded', function () {
1308 if (!class_exists('OxyEl')) {
1309 return;
1310 }
1311 require_once __DIR__ . '/oxygen/oxy-manager.php';
1312 });
1313
1314 add_action('init', 'wpvr_mobile_media_handle');
1315 function wpvr_mobile_media_handle()
1316 {
1317 add_image_size('wpvr_mobile', 4096, 2048); //mobile
1318 }
1319
1320
1321 add_action(
1322 /**
1323 * @param $api \VisualComposer\Modules\Api\Factory
1324 */
1325 'vcv:api',
1326 function ($api) {
1327 $elementsToRegister = [
1328 'wpvrelement',
1329 ];
1330 $pluginBaseUrl = rtrim(WPVR_PLUGIN_DIR_URL, '\\/');
1331 /** @var \VisualComposer\Modules\Elements\ApiController $elementsApi */
1332 $elementsApi = $api->elements;
1333 foreach ($elementsToRegister as $tag) {
1334 $manifestPath = __DIR__ . '/vc/' . $tag . '/manifest.json';
1335 $elementBaseUrl = $pluginBaseUrl . '/vc/' . $tag;
1336 $elementsApi->add($manifestPath, $elementBaseUrl);
1337 }
1338 }
1339 );
1340
1341 function wpvr_redirect_after_activation($plugin)
1342 {
1343 if ($plugin == plugin_basename(__FILE__)) {
1344 $url = admin_url('admin.php?page=rex-wpvr-setup-wizard');
1345 $url = esc_url($url, FILTER_SANITIZE_URL);
1346 exit(wp_safe_redirect($url));
1347 }
1348 }
1349 //add_action('activated_plugin', 'wpvr_redirect_after_activation');
1350
1351 function replace_callback($matches)
1352 {
1353 foreach ($matches as $match) {
1354 return str_replace('<img', '<img decoding="async" ', $match);
1355 }
1356 }
1357
1358
1359 function wpvr_get_explainer_embed_url( $input ) {
1360 $url = esc_url_raw( trim( (string) $input ), array( 'http', 'https' ) );
1361
1362 if ( empty( $url ) || ! preg_match( '#^https?://#i', $url ) ) {
1363 return '';
1364 }
1365
1366 $host = strtolower( (string) wp_parse_url( $url, PHP_URL_HOST ) );
1367 $host = preg_replace( '/^www\./', '', $host );
1368 $path = trim( (string) wp_parse_url( $url, PHP_URL_PATH ), '/' );
1369
1370 if ( 'youtu.be' === $host ) {
1371 $segments = explode( '/', $path );
1372 $video_id = isset( $segments[0] ) ? preg_replace( '/[^A-Za-z0-9_-]/', '', $segments[0] ) : '';
1373
1374 return $video_id ? 'https://www.youtube.com/embed/' . $video_id : '';
1375 }
1376
1377 if ( in_array( $host, array( 'youtube.com', 'm.youtube.com', 'music.youtube.com', 'youtube-nocookie.com' ), true ) ) {
1378 $video_id = '';
1379 $segments = explode( '/', $path );
1380
1381 if ( 'watch' === $path ) {
1382 $query = array();
1383 wp_parse_str( (string) wp_parse_url( $url, PHP_URL_QUERY ), $query );
1384 $video_id = isset( $query['v'] ) ? $query['v'] : '';
1385 } elseif ( isset( $segments[0], $segments[1] ) && in_array( $segments[0], array( 'embed', 'shorts', 'live' ), true ) ) {
1386 $video_id = $segments[1];
1387 }
1388
1389 $video_id = preg_replace( '/[^A-Za-z0-9_-]/', '', (string) $video_id );
1390
1391 return $video_id ? 'https://www.youtube.com/embed/' . $video_id : '';
1392 }
1393
1394 if ( 'vimeo.com' === $host || 'player.vimeo.com' === $host ) {
1395 $segments = array_reverse( array_filter( explode( '/', $path ) ) );
1396
1397 foreach ( $segments as $segment ) {
1398 if ( ctype_digit( $segment ) ) {
1399 return 'https://player.vimeo.com/video/' . $segment;
1400 }
1401 }
1402
1403 return '';
1404 }
1405
1406 return $url;
1407 }
1408
1409 function wpvr_sanitize_iframe_only( $input ) {
1410 // Start with standard allowed post HTML (p, a, strong, etc.)
1411 $allowed_tags = wp_kses_allowed_html( 'post' );
1412
1413 // Explicitly allow <iframe> with specific safe attributes
1414 $allowed_tags['iframe'] = array(
1415 'src' => true,
1416 'width' => true,
1417 'height' => true,
1418 'title' => true,
1419 'frameborder' => true,
1420 'allow' => true,
1421 'allowfullscreen' => true,
1422 'referrerpolicy' => true,
1423 );
1424
1425 $content = trim( (string) $input );
1426
1427 if ( '' === $content ) {
1428 return '';
1429 }
1430
1431 if ( false === stripos( $content, '<iframe' ) ) {
1432 $embed_url = wpvr_get_explainer_embed_url( $content );
1433
1434 if ( $embed_url ) {
1435 $content = sprintf(
1436 '<iframe src="%s" width="100%%" height="100%%" title="Explainer video" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen referrerpolicy="strict-origin-when-cross-origin"></iframe>',
1437 esc_url( $embed_url )
1438 );
1439 }
1440 }
1441
1442 return wp_kses( $content, $allowed_tags );
1443 }
1444
1445 add_filter('fluentform/form_vars_for_JS', 'wpvr_fluent_form_register_inline_vars', 10, 2);
1446 /**
1447 * Register Fluent Forms JS vars safely via WordPress script API.
1448 * Prevents raw script tag concatenation in HTML while ensuring form handlers initialize.
1449 *
1450 * @param array $vars Form configuration variables from Fluent Forms.
1451 * @param object $form Fluent Form database record.
1452 * @return array
1453 */
1454 function wpvr_fluent_form_register_inline_vars($vars, $form) {
1455 if (!empty($vars['form_instance'])) {
1456 $inline_js = 'window.fluent_form_' . esc_js($vars['form_instance']) . ' = ' . wp_json_encode($vars) . ';';
1457 if (wp_script_is('fluent-form-submission', 'done') || wp_doing_ajax()) {
1458 if (function_exists('wp_print_inline_script_tag')) {
1459 wp_print_inline_script_tag($inline_js);
1460 } else {
1461 echo '<script type="text/javascript">' . $inline_js . '</script>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1462 }
1463 } else {
1464 wp_add_inline_script('fluent-form-submission', $inline_js, 'before');
1465 }
1466 }
1467 return $vars;
1468 }
1469