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

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

1,426 lines 53.9 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_render_explainer_button' ) ) {
582 /**
583 * Render the WPVR explainer button independently.
584 *
585 * @param array|null $custom_control
586 * @param array $postdata
587 * @param bool $is_pro
588 * @param bool $autoload
589 * @param string $explainer_right
590 * @param int|string $id
591 *
592 * @return string
593 * @since 8.5.63
594 */
595 function wpvr_render_explainer_button( $custom_control, $postdata, $is_pro, $autoload, $explainer_right, $id ) {
596 $html = '';
597 $explainer_enabled = isset( $postdata['explainerSwitch'] )
598 && in_array( $postdata['explainerSwitch'], array( true, 1, '1', 'on' ), true );
599 $explainer_icon = 'fa fa-video';
600 $explainer_color = '#f7fffb';
601
602 if ( isset( $custom_control ) ) {
603 $explainer_icon = isset( $custom_control['explainerIcon'] ) ? $custom_control['explainerIcon'] : $explainer_icon;
604 $explainer_color = isset( $custom_control['explainerColor'] ) ? $custom_control['explainerColor'] : $explainer_color;
605 } elseif ( isset( $postdata['customcontrol'] ) ) {
606 $raw_control = wpvr_parse_custom_control( $postdata['customcontrol'] );
607 $explainer_icon = isset( $raw_control['explainerIcon'] ) ? $raw_control['explainerIcon'] : $explainer_icon;
608 $explainer_color = isset( $raw_control['explainerColor'] ) ? $raw_control['explainerColor'] : $explainer_color;
609 }
610
611 $pro_license_status = get_option( 'wpvr_edd_license_status' );
612 if ( $explainer_enabled && $pro_license_status === 'valid' && $is_pro ) {
613 $explainer_style = empty( $postdata['explainerContent'] )
614 ? 'pointer-events: none; opacity: 0.5;'
615 : '';
616
617 // Initial display:none to prevent flash for non-autoload tours.
618 $initial_display = ! $autoload ? 'display:none; ' : '';
619
620 $html .= '<div class="explainer_button" id="explainer_button_' . esc_attr( $id ) . '" style="' . $initial_display . 'right:' . esc_attr( $explainer_right ) . '; ' . esc_attr( $explainer_style ) . '">';
621 $html .= '<div class="ctrl" id="explainer_target_' . esc_attr( $id ) . '"><i class="' . esc_attr( $explainer_icon ) . '" style="color:' . esc_attr( $explainer_color ) . ';"></i></div>';
622 $html .= '</div>';
623 }
624
625 return $html;
626 }
627 }
628
629 // Linno telemetry integration
630 function wpvr_block()
631 {
632 wp_register_script(
633 'wpvr-block',
634 plugins_url('build/index.build.js', __FILE__),
635 array('wp-blocks', 'wp-element', 'wp-components', 'wp-editor')
636 );
637
638 if (is_admin()) {
639 wp_enqueue_style(
640 'gutyblocks/guty-block',
641 plugins_url('src/view.css', __FILE__),
642 array()
643 );
644 }
645
646 if (function_exists('register_block_type')) {
647 register_block_type('wpvr/wpvr-block', array(
648 'attributes' => array(
649 'id' => array(
650 'type' => 'string',
651 'default' => '0',
652 ),
653 'width' => array(
654 'type' => 'string',
655 'default' => '600',
656 ),
657 'width_unit' => array(
658 'type' => 'string',
659 'default' => 'px',
660 ),
661 'height' => array(
662 'type' => 'string',
663 'default' => '400',
664 ),
665 'height_unit' => array(
666 'type' => 'string',
667 'default' => 'px',
668 ),
669 'mobile_height' => array(
670 'type' => 'string',
671 'default' => '300',
672 ),
673 'mobile_height_unit' => array(
674 'type' => 'string',
675 'default' => 'px',
676 ),
677 'radius' => array(
678 'type' => 'string',
679 'default' => '0',
680 ),
681 'radius_unit' => array(
682 'type' => 'string',
683 'default' => 'px',
684 ),
685 'border_width' => array(
686 'type' => 'string',
687 'default' => '0',
688 ),
689 'border_style' => array(
690 'type' => 'string',
691 'default' => 'none',
692 ),
693 'border_color' => array(
694 'type' => 'string',
695 'default' => '',
696 ),
697 ),
698 'editor_script' => 'wpvr-block',
699 'render_callback' => 'wpvr_block_render',
700 ));
701 }
702 }
703
704 add_action('init', 'wpvr_block');
705
706 function wpvr_block_render($attributes)
707 {
708 $id = isset($attributes['id']) ? (int) $attributes['id'] : 0;
709 if (!$id) {
710 return '<div class="wpvr-no-tour-selected">' . esc_html__('Please select a tour from the block settings panel.', 'wpvr') . '</div>';
711 }
712
713 $width = isset($attributes['width']) ? $attributes['width'] : '600';
714 if ('fullwidth' === $width) {
715 $width_str = 'fullwidth';
716 } else {
717 $width_unit = isset($attributes['width_unit']) ? $attributes['width_unit'] : 'px';
718 if (preg_match('/(px|%|vw|vh)$/', (string) $width)) {
719 $width_str = $width;
720 } else {
721 $width_str = $width . $width_unit;
722 }
723 }
724
725 $height = isset($attributes['height']) ? $attributes['height'] : '400';
726 $height_unit = isset($attributes['height_unit']) ? $attributes['height_unit'] : 'px';
727 if (!preg_match('/(px|%|vw|vh)$/', (string) $height)) {
728 $height .= $height_unit;
729 }
730
731 $mobile_height = isset($attributes['mobile_height']) ? $attributes['mobile_height'] : '300';
732 $mobile_height_unit = isset($attributes['mobile_height_unit']) ? $attributes['mobile_height_unit'] : 'px';
733 if (!preg_match('/(px|%|vw|vh)$/', (string) $mobile_height)) {
734 $mobile_height .= $mobile_height_unit;
735 }
736
737 $radius = isset($attributes['radius']) ? $attributes['radius'] : '0';
738 $radius_unit = isset($attributes['radius_unit']) ? $attributes['radius_unit'] : 'px';
739 if (!preg_match('/(px|%|vw|vh)$/', (string) $radius)) {
740 $radius .= $radius_unit;
741 }
742
743 $shortcode = sprintf(
744 '[wpvr id="%d" width="%s" height="%s" mobile_height="%s" radius="%s"]',
745 $id,
746 esc_attr($width_str),
747 esc_attr($height),
748 esc_attr($mobile_height),
749 esc_attr($radius)
750 );
751
752 $output = do_shortcode($shortcode);
753
754 // 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).
755 $scripts = '';
756 if (preg_match_all('/<script\b[^>]*>[\s\S]*?<\/script>/i', $output, $matches)) {
757 $scripts = implode("\n", $matches[0]);
758 $output = preg_replace('/<script\b[^>]*>[\s\S]*?<\/script>/i', '', $output);
759 }
760
761 if (!empty($scripts)) {
762 if (wp_doing_ajax() || (defined('REST_REQUEST') && REST_REQUEST)) {
763 $output .= "\n" . $scripts;
764 } else {
765 add_action('wp_footer', function() use ($scripts) {
766 echo $scripts; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
767 }, 20);
768 }
769 }
770
771 $border_css = '';
772 if (!empty($attributes['border_width']) && !empty($attributes['border_style']) && 'none' !== $attributes['border_style']) {
773 $border_width = rtrim((string) $attributes['border_width'], 'px') . 'px';
774 $border_color = !empty($attributes['border_color']) ? $attributes['border_color'] : 'transparent';
775 $border_css = 'border: ' . esc_attr($border_width . ' ' . $attributes['border_style'] . ' ' . $border_color) . ';';
776 }
777
778 $class_name = !empty($attributes['className']) ? ' ' . esc_attr($attributes['className']) : '';
779
780 if ('' !== $border_css || '' !== $class_name) {
781 $wrapper_style = $border_css ? ' style="' . $border_css . ' display: inline-block; width: 100%; max-width: ' . esc_attr('fullwidth' === $width_str ? '100%' : $width_str) . ';"' : '';
782 return '<div class="wpvr-block-wrapper' . $class_name . '"' . $wrapper_style . '>' . $output . '</div>';
783 }
784
785 return $output;
786 }
787
788 function sanitize_content_preserve_styles($content, $allow_forms = false) {
789 // Decode HTML entities first (in case content was encoded in database)
790 $content = html_entity_decode($content, ENT_QUOTES | ENT_HTML5, 'UTF-8');
791
792 // Escape or strip <script> blocks
793 if ($allow_forms) {
794 $content = preg_replace('/<script\b[^>]*>[\s\S]*?<\/script>/i', '', $content);
795 } else {
796 $content = preg_replace_callback('/<script\b[^>]*>(.*?)<\/script>/si', function($matches) {
797 return esc_html($matches[0]); // Convert to plain text
798 }, $content);
799 }
800
801 // Strip dangerous URL-based attributes
802 $content = preg_replace('/(href|action|formaction)\s*=\s*["\']?\s*(javascript|vbscript|data|about):/i', '$1=""', $content);
803
804 // Escape inline event handlers (onclick, onhover, etc.) to display as text
805 $content = preg_replace_callback('/\s*(on\w+)\s*=\s*(["\'])([^"\']*)\2/i', function($matches) {
806 // Escape the attribute value but keep the attribute name visible as text
807 return ' ' . esc_html($matches[1]) . '=' . $matches[2] . esc_html($matches[3]) . $matches[2];
808 }, $content);
809 $content = preg_replace_callback('/\s*(on\w+)\s*=\s*([^>\s]+)/i', function($matches) {
810 // Handle unquoted event handlers
811 return ' ' . esc_html($matches[1]) . '=' . esc_html($matches[2]);
812 }, $content);
813
814 // Remove unsafe embedded/interactive elements
815 if ($allow_forms) {
816 $content = preg_replace('/<(object|embed|applet|frame|frameset|meta|link|base)\b[^>]*>/i', '', $content);
817 $content = preg_replace('/<\/(object|embed|applet|frame|frameset|meta|link|base)>/i', '', $content);
818 } else {
819 $content = preg_replace('/<(object|embed|applet|frame|frameset|meta|link|base|form|input|button|textarea|select|option)\b[^>]*>/i', '', $content);
820 $content = preg_replace('/<\/(object|embed|applet|frame|frameset|meta|link|base|form|input|button|textarea|select|option)>/i', '', $content);
821 }
822
823 // Clean style attributes safely
824 $content = preg_replace_callback('/style\s*=\s*["\']([^"\']*)["\']/', function($matches) {
825 $style = $matches[1];
826 $style = preg_replace('/expression\s*\(/i', '', $style);
827 $style = preg_replace('/(javascript|vbscript|data|about)\s*:/i', '', $style);
828 $style = preg_replace('/url\s*\(\s*["\']?\s*(javascript|vbscript|data):/i', '', $style);
829 $style = preg_replace('/behavior\s*:/i', '', $style);
830 $style = preg_replace('/-moz-binding\s*:/i', '', $style);
831 return 'style="' . esc_attr($style) . '"';
832 }, $content);
833
834 // Sanitize <style> blocks
835 $content = preg_replace_callback('/<style\b[^>]*>(.*?)<\/style>/si', function($matches) {
836 $css = $matches[1];
837 $css = preg_replace('/(expression|javascript|vbscript|data|about)\s*:/i', '', $css);
838 $css = preg_replace('/url\s*\(\s*["\']?\s*(javascript|vbscript|data):/i', '', $css);
839 $css = preg_replace('/behavior\s*:/i', '', $css);
840 $css = preg_replace('/-moz-binding\s*:/i', '', $css);
841 return '<style>' . esc_html($css) . '</style>';
842 }, $content);
843
844 // Allow iframes and styles from safe sources only
845 $allowed_tags = wp_kses_allowed_html('post');
846 $allowed_tags['style'] = [
847 'type' => true,
848 'id' => true,
849 'class' => true,
850 'media' => true,
851 ];
852 $allowed_tags['iframe'] = [
853 'src' => true,
854 'width' => true,
855 'height' => true,
856 'frameborder' => true,
857 'allowfullscreen' => true,
858 'class' => true,
859 'style' => true,
860 'title' => true,
861 'allow' => true,
862 'name' => true,
863 'referrerpolicy' => true,
864 'loading' => true,
865 'sandbox' => true,
866 ];
867 $allowed_tags['img'] = [
868 'src' => true,
869 'alt' => true,
870 'title' => true,
871 'width' => true,
872 'height' => true,
873 'class' => true,
874 'id' => true,
875 'style' => true,
876 'loading' => true,
877 'srcset' => true,
878 'sizes' => true,
879 ];
880
881 if ($allow_forms) {
882 $form_attributes = [
883 'id' => true,
884 'class' => true,
885 'style' => true,
886 'name' => true,
887 'value' => true,
888 'type' => true,
889 'placeholder' => true,
890 'action' => true,
891 'method' => true,
892 'target' => true,
893 'enctype' => true,
894 'disabled' => true,
895 'readonly' => true,
896 'required' => true,
897 'checked' => true,
898 'selected' => true,
899 'multiple' => true,
900 'size' => true,
901 'rows' => true,
902 'cols' => true,
903 'maxlength' => true,
904 'minlength' => true,
905 'min' => true,
906 'max' => true,
907 'step' => true,
908 'pattern' => true,
909 'autocomplete' => true,
910 'autofocus' => true,
911 'for' => true,
912 'data-*' => true,
913 'data-form_id' => true,
914 'data-form_instance' => true,
915 'data-name' => true,
916 'data-type' => true,
917 'aria-invalid' => true,
918 'aria-required' => true,
919 'aria-label' => true,
920 'aria-describedby' => true,
921 'aria-labelledby' => true,
922 ];
923 $allowed_tags['form'] = $form_attributes;
924 $allowed_tags['input'] = $form_attributes;
925 $allowed_tags['button'] = $form_attributes;
926 $allowed_tags['textarea'] = $form_attributes;
927 $allowed_tags['select'] = $form_attributes;
928 $allowed_tags['option'] = $form_attributes;
929 $allowed_tags['optgroup'] = $form_attributes;
930 $allowed_tags['label'] = $form_attributes;
931 $allowed_tags['fieldset'] = $form_attributes;
932 $allowed_tags['legend'] = $form_attributes;
933 if (!isset($allowed_tags['div'])) {
934 $allowed_tags['div'] = [];
935 }
936 $allowed_tags['div']['data-*'] = true;
937 $allowed_tags['div']['data-form_id'] = true;
938 $allowed_tags['div']['data-form_instance'] = true;
939 if (!isset($allowed_tags['span'])) {
940 $allowed_tags['span'] = [];
941 }
942 $allowed_tags['span']['data-*'] = true;
943 }
944
945 // Apply wp_kses() to keep only allowed tags/attributes
946 $content = wp_kses($content, $allowed_tags);
947
948 // Finally, validate iframe src for security (allow https only, block javascript: etc.)
949 $content = preg_replace_callback('/<iframe[^>]+src=["\']([^"\']+)["\'][^>]*>(?:<\/iframe>)?/i', function($matches) {
950 $src = $matches[1];
951 // Allow any https:// or http:// URL, but block javascript:, data:, vbscript:, etc.
952 if (preg_match('/^(https?:)?\/\//i', $src) && !preg_match('/^(javascript|data|vbscript|about):/i', $src)) {
953 return $matches[0]; // keep safe iframe
954 }
955 // Strip unsafe iframe
956 return '';
957 }, $content);
958
959 return $content;
960 }
961 function wpvr_hex2rgb($colour)
962 {
963 if (isset($colour[0]) && $colour[0] == '#') {
964 $colour = substr($colour, 1);
965 }
966 if (strlen($colour) == 6) {
967 list($r, $g, $b) = array($colour[0] . $colour[1], $colour[2] . $colour[3], $colour[4] . $colour[5]);
968 } elseif (strlen($colour) == 3) {
969 list($r, $g, $b) = array($colour[0] . $colour[0], $colour[1] . $colour[1], $colour[2] . $colour[2]);
970 } else {
971 return false;
972 }
973 $r = hexdec($r);
974 $g = hexdec($g);
975 $b = hexdec($b);
976 return array($r . ', ' . $g . ', ' . $b);
977 }
978
979 function wpvr_HTMLToRGB($htmlCode)
980 {
981 $r = 0;
982 $g = 0;
983 $b = 0;
984 if (isset($htmlCode[0]) && $htmlCode[0] == '#') {
985 $htmlCode = substr($htmlCode, 1);
986 }
987
988 if (strlen($htmlCode) == 3) {
989 $htmlCode = $htmlCode[0] . $htmlCode[0] . $htmlCode[1] . $htmlCode[1] . $htmlCode[2] . $htmlCode[2];
990 }
991
992 if (isset($htmlCode[0]) && isset($htmlCode[1])) {
993 $r = hexdec($htmlCode[0] . $htmlCode[1]);
994 }
995 if (isset($htmlCode[2]) && isset($htmlCode[3])) {
996 $g = hexdec($htmlCode[2] . $htmlCode[3]);
997 }
998 if (isset($htmlCode[4]) && isset($htmlCode[5])) {
999 $b = hexdec($htmlCode[4] . $htmlCode[5]);
1000 }
1001
1002 return $b + ($g << 0x8) + ($r << 0x10);
1003 }
1004
1005 function wpvr_RGBToHSL($RGB)
1006 {
1007 $r = 0xFF & ($RGB >> 0x10);
1008 $g = 0xFF & ($RGB >> 0x8);
1009 $b = 0xFF & $RGB;
1010
1011 $r = ((float)$r) / 255.0;
1012 $g = ((float)$g) / 255.0;
1013 $b = ((float)$b) / 255.0;
1014
1015 $maxC = max($r, $g, $b);
1016 $minC = min($r, $g, $b);
1017
1018 $l = ($maxC + $minC) / 2.0;
1019
1020 if ($maxC == $minC) {
1021 $s = 0;
1022 $h = 0;
1023 } else {
1024 if ($l < .5) {
1025 $s = ($maxC - $minC) / ($maxC + $minC);
1026 } else {
1027 $s = ($maxC - $minC) / (2.0 - $maxC - $minC);
1028 }
1029 if ($r == $maxC) {
1030 $h = ($g - $b) / ($maxC - $minC);
1031 }
1032 if ($g == $maxC) {
1033 $h = 2.0 + ($b - $r) / ($maxC - $minC);
1034 }
1035 if ($b == $maxC) {
1036 $h = 4.0 + ($r - $g) / ($maxC - $minC);
1037 }
1038
1039 $h = $h / 6.0;
1040 }
1041
1042 $h = (int)round(255.0 * $h);
1043 $s = (int)round(255.0 * $s);
1044 $l = (int)round(255.0 * $l);
1045
1046 return (object) array('hue' => $h, 'saturation' => $s, 'lightness' => $l);
1047 }
1048
1049 add_action('rest_api_init', 'wpvr_rest_data_route');
1050 function wpvr_rest_data_route()
1051 {
1052 register_rest_route('wpvr/v1', '/panodata/', array(
1053 'methods' => 'GET',
1054 'callback' => 'wpvr_rest_data_set',
1055 'permission_callback' => 'wpvr_rest_route_permission'
1056 ));
1057 }
1058
1059 function wpvr_rest_route_permission()
1060 {
1061 $post_type_obj = get_post_type_object( 'wpvr_item' );
1062 $edit_cap = $post_type_obj ? $post_type_obj->cap->edit_posts : 'edit_wpvr_tours';
1063 return current_user_can( $edit_cap ) || current_user_can( 'edit_posts' );
1064 }
1065
1066 function wpvr_rest_data_set()
1067 {
1068 $post_type_obj = get_post_type_object( 'wpvr_item' );
1069 $edit_cap = $post_type_obj ? $post_type_obj->cap->edit_posts : 'edit_wpvr_tours';
1070 $post_status = current_user_can( $edit_cap ) ? array( 'publish', 'draft', 'private' ) : 'publish';
1071
1072 $query = new WP_Query(array(
1073 'post_type' => 'wpvr_item',
1074 'post_status' => $post_status,
1075 'posts_per_page' => -1,
1076 ));
1077
1078 $wpvr_list = array();
1079 $list_none = array('value' => 0, 'label' => 'None');
1080 array_push($wpvr_list, $list_none);
1081 while ($query->have_posts()) {
1082 $query->the_post();
1083 $title = mb_convert_encoding(get_the_title(), 'UTF-8', 'HTML-ENTITIES');
1084 $post_id = get_the_ID();
1085 $title = $post_id . ' : ' . $title;
1086 $list_ob = array('value' => $post_id, 'label' => $title);
1087 array_push($wpvr_list, $list_ob);
1088 }
1089 wp_reset_postdata();
1090
1091 return $wpvr_list;
1092 }
1093
1094 function wpvr_isMobileDevice()
1095 {
1096 return preg_match("/(android|avantgo|blackberry|bolt|boost|cricket|docomo|fone|hiptop|mini|mobi|palm|phone|pie|tablet|up\.browser|up\.link|webos|wos)/i", $_SERVER["HTTP_USER_AGENT"]);
1097 }
1098
1099 function wpvr_directory()
1100 {
1101 $upload = wp_upload_dir();
1102 $upload_dir = $upload['basedir'];
1103 $upload_dir_temp = $upload_dir . '/wpvr/temp/';
1104 if (!is_dir($upload_dir_temp)) {
1105 wp_mkdir_p($upload_dir_temp, 0700);
1106 }
1107 }
1108
1109 add_action('admin_init', 'wpvr_directory');
1110
1111
1112 function wpvr_add_role_cap()
1113 {
1114 $editor_active = get_option('wpvr_editor_active');
1115
1116 $author_active = get_option('wpvr_author_active');
1117
1118 $admin = get_role('administrator');
1119 $admin->add_cap('publish_wpvr_tour');
1120 $admin->add_cap('edit_wpvr_tours');
1121 $admin->add_cap('read_wpvr_tour');
1122 $admin->add_cap('edit_wpvr_tour');
1123 $admin->add_cap('edit_wpvr_tours');
1124 $admin->add_cap('publish_wpvr_tours');
1125 $admin->add_cap('publish_wpvr_tour');
1126 $admin->add_cap('delete_wpvr_tour');
1127 $admin->add_cap('edit_other_wpvr_tours');
1128 $admin->add_cap('delete_other_wpvr_tours');
1129
1130 if ($editor_active == "true") {
1131 $editor = get_role('editor');
1132 if ($editor) {
1133 $editor->add_cap('publish_wpvr_tour');
1134 $editor->add_cap('edit_wpvr_tours');
1135 $editor->add_cap('read_wpvr_tour');
1136 $editor->add_cap('edit_wpvr_tour');
1137 $editor->add_cap('edit_wpvr_tours');
1138 $editor->add_cap('publish_wpvr_tours');
1139 $editor->add_cap('publish_wpvr_tour');
1140 $editor->add_cap('delete_wpvr_tour');
1141 $editor->add_cap('edit_other_wpvr_tours');
1142 $editor->add_cap('delete_other_wpvr_tours');
1143 }
1144 } else {
1145 $editor = get_role('editor');
1146 if ($editor) {
1147 $editor->remove_cap('publish_wpvr_tour');
1148 $editor->remove_cap('edit_wpvr_tours');
1149 $editor->remove_cap('read_wpvr_tour');
1150 $editor->remove_cap('edit_wpvr_tour');
1151 $editor->remove_cap('edit_wpvr_tours');
1152 $editor->remove_cap('publish_wpvr_tours');
1153 $editor->remove_cap('publish_wpvr_tour');
1154 $editor->remove_cap('delete_wpvr_tour');
1155 $editor->remove_cap('edit_other_wpvr_tours');
1156 $editor->remove_cap('delete_other_wpvr_tours');
1157 }
1158 }
1159
1160 if ($author_active == "true") {
1161 $author = get_role('author');
1162 if ($author) {
1163 $author->add_cap('read_wpvr_tour');
1164 $author->add_cap('edit_wpvr_tour');
1165 $author->add_cap('edit_wpvr_tours');
1166 $author->add_cap('publish_wpvr_tours');
1167 $author->add_cap('publish_wpvr_tour');
1168 $author->add_cap('delete_wpvr_tour');
1169 }
1170 } else {
1171 $author = get_role('author');
1172 if ($author) {
1173 $author->remove_cap('read_wpvr_tour');
1174 $author->remove_cap('edit_wpvr_tour');
1175 $author->remove_cap('edit_wpvr_tours');
1176 $author->remove_cap('publish_wpvr_tours');
1177 $author->remove_cap('publish_wpvr_tour');
1178 $author->remove_cap('delete_wpvr_tour');
1179 }
1180 }
1181
1182 if(is_plugin_active( 'dokan-lite/dokan.php' ) || is_plugin_active( 'dokan-pro/dokan.php' )){
1183 $dokan_vendor_active = get_option('dokan_vendor_active');
1184
1185 if( 'true' === $dokan_vendor_active){
1186 $seller = get_role('seller');
1187 if ($seller) {
1188 $seller->add_cap('read_wpvr_tour');
1189 $seller->add_cap('edit_wpvr_tour');
1190 $seller->add_cap('edit_wpvr_tours');
1191 $seller->add_cap('publish_wpvr_tours');
1192 $seller->add_cap('publish_wpvr_tour');
1193 $seller->add_cap('delete_wpvr_tour');
1194 }
1195 } else{
1196 $seller = get_role('seller');
1197 if ($seller) {
1198 $seller->remove_cap('read_wpvr_tour');
1199 $seller->remove_cap('edit_wpvr_tour');
1200 $seller->remove_cap('edit_wpvr_tours');
1201 $seller->remove_cap('publish_wpvr_tours');
1202 $seller->remove_cap('publish_wpvr_tour');
1203 $seller->remove_cap('delete_wpvr_tour');
1204 }
1205 }
1206 }
1207
1208
1209 }
1210
1211 add_action('admin_init', 'wpvr_add_role_cap', 999);
1212
1213 function wpvr_role_management_from_post_type($args, $post_type)
1214 {
1215 if ('wpvr_item' !== $post_type) {
1216 return $args;
1217 }
1218
1219 $editor_active = get_option('wpvr_editor_active');
1220 $author_active = get_option('wpvr_author_active');
1221 $user = wp_get_current_user();
1222
1223 if ($editor_active == "true") {
1224 if (in_array('editor', (array) $user->roles)) {
1225 $args['show_in_menu'] = true;
1226 }
1227 }
1228
1229 if ($author_active == "true") {
1230 if (in_array('author', (array) $user->roles)) {
1231 $args['show_in_menu'] = true;
1232 }
1233 }
1234
1235 if(is_plugin_active( 'dokan-lite/dokan.php' ) || is_plugin_active( 'dokan-pro/dokan.php' )){
1236 $dokan_vendor_active = get_option('dokan_vendor_active');
1237 if( 'true' === $dokan_vendor_active){
1238 if (in_array('seller', (array) $user->roles)) {
1239 $args['show_in_menu'] = true;
1240 }
1241 }
1242
1243 }
1244
1245 return $args;
1246 }
1247 add_filter('register_post_type_args', 'wpvr_role_management_from_post_type', 10, 2);
1248
1249 function wpvr_cache_admin_notice()
1250 {
1251 $option = get_option('wpvr_warning');
1252 if (!$option) {
1253 ?>
1254 <div class="notice notice-warning" id="wpvr-warning" style="position: relative;">
1255 <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>
1256 <button type="button" id="wpvr-dismissible" class="notice-dismiss"><span class="screen-reader-text">Dismiss this notice.</span></button>
1257 </div>
1258 <?php
1259 }
1260 }
1261 // add_action('admin_notices', 'wpvr_cache_admin_notice');
1262
1263 //===Oxygen widget===//
1264 add_action('plugins_loaded', function () {
1265 if (!class_exists('OxyEl')) {
1266 return;
1267 }
1268 require_once __DIR__ . '/oxygen/oxy-manager.php';
1269 });
1270
1271 add_action('init', 'wpvr_mobile_media_handle');
1272 function wpvr_mobile_media_handle()
1273 {
1274 add_image_size('wpvr_mobile', 4096, 2048); //mobile
1275 }
1276
1277
1278 add_action(
1279 /**
1280 * @param $api \VisualComposer\Modules\Api\Factory
1281 */
1282 'vcv:api',
1283 function ($api) {
1284 $elementsToRegister = [
1285 'wpvrelement',
1286 ];
1287 $pluginBaseUrl = rtrim(WPVR_PLUGIN_DIR_URL, '\\/');
1288 /** @var \VisualComposer\Modules\Elements\ApiController $elementsApi */
1289 $elementsApi = $api->elements;
1290 foreach ($elementsToRegister as $tag) {
1291 $manifestPath = __DIR__ . '/vc/' . $tag . '/manifest.json';
1292 $elementBaseUrl = $pluginBaseUrl . '/vc/' . $tag;
1293 $elementsApi->add($manifestPath, $elementBaseUrl);
1294 }
1295 }
1296 );
1297
1298 function wpvr_redirect_after_activation($plugin)
1299 {
1300 if ($plugin == plugin_basename(__FILE__)) {
1301 $url = admin_url('admin.php?page=rex-wpvr-setup-wizard');
1302 $url = esc_url($url, FILTER_SANITIZE_URL);
1303 exit(wp_safe_redirect($url));
1304 }
1305 }
1306 //add_action('activated_plugin', 'wpvr_redirect_after_activation');
1307
1308 function replace_callback($matches)
1309 {
1310 foreach ($matches as $match) {
1311 return str_replace('<img', '<img decoding="async" ', $match);
1312 }
1313 }
1314
1315
1316 function wpvr_get_explainer_embed_url( $input ) {
1317 $url = esc_url_raw( trim( (string) $input ), array( 'http', 'https' ) );
1318
1319 if ( empty( $url ) || ! preg_match( '#^https?://#i', $url ) ) {
1320 return '';
1321 }
1322
1323 $host = strtolower( (string) wp_parse_url( $url, PHP_URL_HOST ) );
1324 $host = preg_replace( '/^www\./', '', $host );
1325 $path = trim( (string) wp_parse_url( $url, PHP_URL_PATH ), '/' );
1326
1327 if ( 'youtu.be' === $host ) {
1328 $segments = explode( '/', $path );
1329 $video_id = isset( $segments[0] ) ? preg_replace( '/[^A-Za-z0-9_-]/', '', $segments[0] ) : '';
1330
1331 return $video_id ? 'https://www.youtube.com/embed/' . $video_id : '';
1332 }
1333
1334 if ( in_array( $host, array( 'youtube.com', 'm.youtube.com', 'music.youtube.com', 'youtube-nocookie.com' ), true ) ) {
1335 $video_id = '';
1336 $segments = explode( '/', $path );
1337
1338 if ( 'watch' === $path ) {
1339 $query = array();
1340 wp_parse_str( (string) wp_parse_url( $url, PHP_URL_QUERY ), $query );
1341 $video_id = isset( $query['v'] ) ? $query['v'] : '';
1342 } elseif ( isset( $segments[0], $segments[1] ) && in_array( $segments[0], array( 'embed', 'shorts', 'live' ), true ) ) {
1343 $video_id = $segments[1];
1344 }
1345
1346 $video_id = preg_replace( '/[^A-Za-z0-9_-]/', '', (string) $video_id );
1347
1348 return $video_id ? 'https://www.youtube.com/embed/' . $video_id : '';
1349 }
1350
1351 if ( 'vimeo.com' === $host || 'player.vimeo.com' === $host ) {
1352 $segments = array_reverse( array_filter( explode( '/', $path ) ) );
1353
1354 foreach ( $segments as $segment ) {
1355 if ( ctype_digit( $segment ) ) {
1356 return 'https://player.vimeo.com/video/' . $segment;
1357 }
1358 }
1359
1360 return '';
1361 }
1362
1363 return $url;
1364 }
1365
1366 function wpvr_sanitize_iframe_only( $input ) {
1367 // Start with standard allowed post HTML (p, a, strong, etc.)
1368 $allowed_tags = wp_kses_allowed_html( 'post' );
1369
1370 // Explicitly allow <iframe> with specific safe attributes
1371 $allowed_tags['iframe'] = array(
1372 'src' => true,
1373 'width' => true,
1374 'height' => true,
1375 'title' => true,
1376 'frameborder' => true,
1377 'allow' => true,
1378 'allowfullscreen' => true,
1379 'referrerpolicy' => true,
1380 );
1381
1382 $content = trim( (string) $input );
1383
1384 if ( '' === $content ) {
1385 return '';
1386 }
1387
1388 if ( false === stripos( $content, '<iframe' ) ) {
1389 $embed_url = wpvr_get_explainer_embed_url( $content );
1390
1391 if ( $embed_url ) {
1392 $content = sprintf(
1393 '<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>',
1394 esc_url( $embed_url )
1395 );
1396 }
1397 }
1398
1399 return wp_kses( $content, $allowed_tags );
1400 }
1401
1402 add_filter('fluentform/form_vars_for_JS', 'wpvr_fluent_form_register_inline_vars', 10, 2);
1403 /**
1404 * Register Fluent Forms JS vars safely via WordPress script API.
1405 * Prevents raw script tag concatenation in HTML while ensuring form handlers initialize.
1406 *
1407 * @param array $vars Form configuration variables from Fluent Forms.
1408 * @param object $form Fluent Form database record.
1409 * @return array
1410 */
1411 function wpvr_fluent_form_register_inline_vars($vars, $form) {
1412 if (!empty($vars['form_instance'])) {
1413 $inline_js = 'window.fluent_form_' . esc_js($vars['form_instance']) . ' = ' . wp_json_encode($vars) . ';';
1414 if (wp_script_is('fluent-form-submission', 'done') || wp_doing_ajax()) {
1415 if (function_exists('wp_print_inline_script_tag')) {
1416 wp_print_inline_script_tag($inline_js);
1417 } else {
1418 echo '<script type="text/javascript">' . $inline_js . '</script>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1419 }
1420 } else {
1421 wp_add_inline_script('fluent-form-submission', $inline_js, 'before');
1422 }
1423 }
1424 return $vars;
1425 }
1426