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 / src / Api / Transformers / TourTransformer.php

TourTransformer.php in WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress 9.1.3, at src/Api/Transformers/TourTransformer.php

690 lines 39.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace RexTheme\WPVR\Api\Transformers;
4
5 use RexTheme\WPVR\Api\Contracts\TransformerInterface;
6
7 class TourTransformer implements TransformerInterface {
8
9 protected bool $is_pro;
10
11 public function __construct( bool $is_pro = false ) {
12 $this->is_pro = $is_pro;
13 }
14
15 /**
16 * Convert raw panodata PHP array → normalized API JSON shape.
17 */
18 public function toApi( array $raw ): array {
19 if ( ! $this->is_pro ) {
20 $raw = wpvr_get_effective_panodata( $raw );
21 }
22
23 $scenes = $this->scenes_to_api( $raw['panodata']['scene-list'] ?? [] );
24 // Enabled: check new key first, then fall back to legacy `autoRotate` speed (truthy = enabled).
25 $auto_rotate_enabled = ( $raw['autorotate-enabled'] ?? '' ) === 'on' || ! empty( $raw['autoRotate'] );
26 // Speed: new key first, then legacy `autoRotate` (which doubles as speed in old tours).
27 $auto_rotate_speed = isset( $raw['autorotate-speed'] )
28 ? (float) $raw['autorotate-speed']
29 : ( isset( $raw['autoRotate'] ) ? (float) $raw['autoRotate'] : -5 );
30 // Delay: new key first, then legacy `autoRotateInactivityDelay`.
31 $auto_rotate_delay = isset( $raw['autorotate-delay'] )
32 ? (int) $raw['autorotate-delay']
33 : ( isset( $raw['autoRotateInactivityDelay'] ) ? (int) $raw['autoRotateInactivityDelay'] : 2000 );
34 $auto_rotate = [
35 'enabled' => $auto_rotate_enabled,
36 'speed' => $auto_rotate_speed,
37 'delay' => $auto_rotate_delay,
38 'stopDelay' => isset( $raw['autorotationstopdelay'] ) && $raw['autorotationstopdelay'] !== ''
39 ? (int) $raw['autorotationstopdelay']
40 : ( isset( $raw['autoRotateStopDelay'] ) && $raw['autoRotateStopDelay'] !== '' ? (int) $raw['autoRotateStopDelay'] : null ),
41 ];
42
43 $default_scene_id = ! empty( $raw['defaultscene'] ) ? $raw['defaultscene'] : null;
44 $has_explicit_default_scene = ( $raw['defaultscene-auto'] ?? 'off' ) !== 'on';
45
46 return [
47 'defaultSceneId' => $has_explicit_default_scene ? $default_scene_id : null,
48 'tourType' => $this->detect_tour_type( $raw ),
49 'settings' => [
50 'autoLoad' => ! empty( $raw['autoLoad'] ),
51 'showControls' => isset( $raw['showControls'] ) ? (bool) $raw['showControls'] : true,
52 'previewText' => $raw['previewtext'] ?? '',
53 'previewImage' => $raw['preview'] ?? '',
54 'sceneFadeDuration' => isset( $raw['scenefadeduration'] ) ? (int) $raw['scenefadeduration'] : 0,
55 'showSceneInfo' => ( $raw['scene-info-enabled'] ?? 'on' ) !== 'off',
56 'autoRotate' => $auto_rotate,
57 'socialShare' => ( $raw['wpvr_social_share'] ?? 'off' ) === 'on',
58 ],
59 'floorPlan' => $this->floor_plan_to_api( $raw ),
60 'backgroundTour' => [
61 'enabled' => ( $raw['bg_tour_enabler'] ?? 'off' ) === 'on',
62 'title' => $raw['bg_tour_title'] ?? '',
63 'subtitle' => $raw['bg_tour_subtitle'] ?? '',
64 ],
65 'videoData' => [
66 'url' => $raw['vidurl'] ?? '',
67 'autoplay' => ( $raw['video-autoplay'] ?? $raw['autoplay'] ?? 'off' ) === 'on',
68 'loop' => ( $raw['video-loop'] ?? $raw['loop'] ?? 'off' ) === 'on',
69 ],
70 'streetViewData' => [
71 'embedUrl' => $raw['streetviewurl'] ?? '',
72 ],
73 'scenes' => $scenes,
74 'advancedSettings' => $this->advanced_settings_to_api( $raw ),
75 ];
76 }
77
78 /**
79 * Convert normalized API JSON → raw panodata PHP array.
80 */
81 public function fromApi( array $data ): array {
82 $settings = $data['settings'] ?? [];
83 $floor_plan = $data['floorPlan'] ?? [];
84 $bg_tour = $data['backgroundTour'] ?? [];
85 $auto_rotate = $settings['autoRotate'] ?? [];
86
87 $video_data = $data['videoData'] ?? [];
88 $street_view_data = $data['streetViewData'] ?? [];
89 $advanced_control = is_array( $data['advancedControl'] ?? null )
90 ? $data['advancedControl']
91 : ( is_array( $data['proData'] ?? null ) ? $data['proData'] : [] );
92 $tour_type = in_array( $data['tourType'] ?? 'image', [ 'image', 'video', 'street-view' ], true )
93 ? $data['tourType']
94 : 'image';
95 if ( ! $this->is_pro && $tour_type === 'street-view' ) {
96 $tour_type = 'image';
97 $street_view_data = [];
98 }
99
100 $scenes = is_array( $data['scenes'] ?? null ) ? $data['scenes'] : [];
101 $requested_default_scene_id = (string) ( $data['defaultSceneId'] ?? '' );
102 $has_explicit_default_scene = $requested_default_scene_id !== ''
103 && ! empty( array_filter(
104 $scenes,
105 static function ( $scene ) use ( $requested_default_scene_id ) {
106 return is_array( $scene ) && ( $scene['id'] ?? '' ) === $requested_default_scene_id;
107 }
108 ) );
109 $default_scene_id = $has_explicit_default_scene
110 ? $requested_default_scene_id
111 : ( $scenes[0]['id'] ?? '' );
112
113 $raw = [
114 'autoLoad' => ! empty( $settings['autoLoad'] ) && $settings['autoLoad'] !== 'off' && $settings['autoLoad'] !== 'false',
115 'showControls' => ! empty( $settings['showControls'] ),
116 'draggable' => ( isset( $advanced_control['draggable'] ) && ( $advanced_control['draggable'] === 'off' || $advanced_control['draggable'] === false ) ) ? 'off' : 'on',
117 'mouseZoom' => ( isset( $advanced_control['mouseZoom'] ) && ( $advanced_control['mouseZoom'] === 'off' || $advanced_control['mouseZoom'] === false ) ) ? 'off' : 'on',
118 'diskeyboard' => ( isset( $advanced_control['diskeyboard'] ) && ( $advanced_control['diskeyboard'] === 'off' || $advanced_control['diskeyboard'] === false ) ) ? 'on' : 'off',
119 'keyboardzoom' => ( isset( $advanced_control['keyboardzoom'] ) && ( $advanced_control['keyboardzoom'] === 'off' || $advanced_control['keyboardzoom'] === false ) ) ? false : true,
120 'previewtext' => $settings['previewText'] ?? '',
121 'scenefadeduration' => isset( $settings['sceneFadeDuration'] ) ? (string) $settings['sceneFadeDuration'] : '0',
122 'scene-info-enabled' => array_key_exists( 'showSceneInfo', $settings ) && ! $settings['showSceneInfo'] ? 'off' : 'on',
123 'defaultscene' => $default_scene_id,
124 'defaultscene-auto' => $has_explicit_default_scene ? 'off' : 'on',
125 'autorotate-enabled' => ! empty( $auto_rotate['enabled'] ) ? 'on' : 'off',
126 'autorotate-speed' => $auto_rotate['speed'] ?? -5,
127 'autorotate-delay' => $auto_rotate['delay'] ?? 2000,
128 'autorotationstopdelay' => $auto_rotate['stopDelay'] ?? '',
129 // Legacy keys for PRO plugin / shortcode compatibility.
130 'autoRotate' => ! empty( $auto_rotate['enabled'] ) ? ( $auto_rotate['speed'] ?? -5 ) : '',
131 'autoRotateInactivityDelay' => $auto_rotate['delay'] ?? 2000,
132 'autoRotateStopDelay' => $auto_rotate['stopDelay'] ?? '',
133 'floorplan-enabled' => ! empty( $floor_plan['enabled'] ) ? 'on' : 'off',
134 'floorplan-image' => $floor_plan['imageUrl'] ?? '',
135 'floorplan-compass' => ! empty( $floor_plan['directionIndicator']['enabled'] ) ? 'on' : 'off',
136 'floorplan-compass-color' => $floor_plan['directionIndicator']['color'] ?? '#6D28D9',
137 // Legacy keys — used by legacy rendering and pro plugin.
138 'floor_plan_tour_enabler' => ! empty( $floor_plan['enabled'] ) ? 'on' : 'off',
139 'floor_plan_attachment_url' => $floor_plan['imageUrl'] ?? '',
140 'floor_plan_custom_color' => ! empty( $floor_plan['pointerColor'] ) ? $floor_plan['pointerColor'] : '#cca92c',
141 'floor_plan_direction_indicator' => ! empty( $floor_plan['directionIndicator']['enabled'] ) ? 'on' : 'off',
142 'floor_plan_pointer_position' => $this->pointer_positions_from_api( $floor_plan ),
143 'floor_plan_data_list' => $this->pointer_data_list_from_api( $floor_plan ),
144 'bg_tour_enabler' => ! empty( $bg_tour['enabled'] ) ? 'on' : 'off',
145 'bg_tour_title' => $bg_tour['title'] ?? '',
146 'bg_tour_subtitle' => $bg_tour['subtitle'] ?? '',
147 'genericform' => ! empty( $advanced_control['genericform'] ) && $advanced_control['genericform'] !== 'off' ? 'on' : 'off',
148 'genericformshortcode' => sanitize_text_field( (string) ( $advanced_control['genericformshortcode'] ?? '' ) ),
149 'genericformicon' => sanitize_text_field( (string) ( $advanced_control['genericformicon'] ?? 'fab fa-wpforms' ) ),
150 'genericformiconcolor' => sanitize_hex_color( $advanced_control['genericformiconcolor'] ?? '' ) ?: '#f7fffb',
151 'calltoaction' => ! empty( $advanced_control['calltoaction'] ) && $advanced_control['calltoaction'] !== 'off' ? 'on' : 'off',
152 'buttontext' => sanitize_text_field( (string) ( $advanced_control['buttontext'] ?? 'Click Here' ) ),
153 'buttonurl' => esc_url_raw( (string) ( $advanced_control['buttonurl'] ?? '' ) ),
154 'button_configuration' => $this->button_configuration_to_api( $advanced_control['button_configuration'] ?? [] ),
155 'preview' => esc_url_raw( $settings['previewImage'] ?? '' ),
156 'panoid' => '',
157 'customcontrol' => $this->customcontrol_to_api(
158 is_array( $advanced_control['customcontrol'] ?? null )
159 ? $advanced_control['customcontrol']
160 : []
161 ),
162 'tour-type' => $tour_type,
163 'vidurl' => esc_url_raw( $video_data['url'] ?? '' ),
164 'video-autoplay' => ! empty( $video_data['autoplay'] ) ? 'on' : 'off',
165 'video-loop' => ! empty( $video_data['loop'] ) ? 'on' : 'off',
166 'autoplay' => ! empty( $video_data['autoplay'] ) ? 'on' : 'off',
167 'loop' => ! empty( $video_data['loop'] ) ? 'on' : 'off',
168 'streetviewurl' => esc_url_raw( $street_view_data['embedUrl'] ?? '' ),
169 'streetview' => ! empty( $street_view_data['embedUrl'] ) ? 'on' : 'off',
170 'panodata' => [
171 'scene-list' => $this->scenes_from_api(
172 $scenes,
173 $default_scene_id,
174 ( $settings['showSceneInfo'] ?? true ) !== false
175 ),
176 ],
177 'wpvr_social_share' => ! empty( $settings['socialShare'] ) ? 'on' : 'off',
178 ];
179
180 if ( $tour_type === 'video' && ! empty( $video_data['url'] ) ) {
181 $raw['vidid'] = 'vid' . ( $data['tourId'] ?? $data['id'] ?? wp_rand( 1000, 99999 ) );
182 }
183
184 return $raw;
185 }
186
187 // -------------------------------------------------------------------------
188
189 private function floor_plan_to_api( array $raw ): array {
190 // Merge pointer positions and scene assignments into a unified array.
191 $positions = $raw['floor_plan_pointer_position'] ?? [];
192 $data_list = $raw['floor_plan_data_list'] ?? [];
193
194 // Build id → sceneId map from data_list ({id:'1', name:'plan1', value:'scene-id'}).
195 $scene_map = [];
196 foreach ( $data_list as $item ) {
197 $item = is_object( $item ) ? $item : (object) $item;
198 $scene_map[ $item->id ?? '' ] = $item->value ?? '';
199 }
200
201 $pointers = [];
202 foreach ( $positions as $pos ) {
203 $pos = is_object( $pos ) ? $pos : (object) $pos;
204 $pos_id = $pos->id ?? '';
205 // Extract numeric suffix from 'pointer-N'.
206 preg_match( '/(\d+)$/', $pos_id, $m );
207 $num = $m[1] ?? '';
208 $scene_id = $scene_map[ $num ] ?? $scene_map[ $pos_id ] ?? '';
209
210 $pointers[] = [
211 'id' => $pos_id,
212 'top' => $pos->data_top ?? '0%',
213 'left' => $pos->data_left ?? '0%',
214 'sceneId' => $scene_id,
215 ];
216 }
217
218 return [
219 // Read from new key first, fall back to legacy key.
220 'enabled' => ( $raw['floorplan-enabled'] ?? 'off' ) === 'on' || ( $raw['floor_plan_tour_enabler'] ?? 'off' ) === 'on',
221 'imageUrl' => $raw['floorplan-image'] ?? $raw['floor_plan_attachment_url'] ?? '',
222 'pointerColor' => ! empty( $raw['floor_plan_custom_color'] ) ? $raw['floor_plan_custom_color'] : '#cca92c',
223 'pointers' => $pointers,
224 'directionIndicator' => [
225 'enabled' => ( $raw['floorplan-compass'] ?? 'off' ) === 'on' || ( $raw['floor_plan_direction_indicator'] ?? 'off' ) === 'on',
226 'color' => $raw['floorplan-compass-color'] ?? '#6D28D9',
227 ],
228 ];
229 }
230
231 private function pointer_positions_from_api( array $floor_plan ): array {
232 $pointers = $floor_plan['pointers'] ?? [];
233 $color = $floor_plan['pointerColor'] ?? '#cca92c';
234 $result = [];
235 $index = 1;
236 foreach ( $pointers as $pointer ) {
237 $top = $pointer['top'] ?? '0%';
238 $left = $pointer['left'] ?? '0%';
239 $result[] = (object) [
240 'id' => 'pointer-' . $index,
241 'text' => (string) $index,
242 'data_top' => $top,
243 'data_left' => $left,
244 'style' => "background:{$color};top:{$top};left:{$left};",
245 ];
246 $index++;
247 }
248 return $result;
249 }
250
251 private function pointer_data_list_from_api( array $floor_plan ): array {
252 $pointers = $floor_plan['pointers'] ?? [];
253 $result = [];
254 $index = 1;
255 foreach ( $pointers as $pointer ) {
256 $result[] = (object) [
257 'id' => (string) $index,
258 'name' => 'plan' . $index,
259 'value' => $pointer['sceneId'] ?? '',
260 ];
261 $index++;
262 }
263 return $result;
264 }
265
266 /**
267 * Detect tour type supporting both new-UI ('tour-type' key) and legacy
268 * ('streetviewdata'/'vidid' keys) save formats.
269 */
270 private function detect_tour_type( array $raw ): string {
271 if ( ! empty( $raw['tour-type'] ) ) {
272 $allowed = [ 'image', 'video', 'street-view' ];
273 return in_array( $raw['tour-type'], $allowed, true ) ? $raw['tour-type'] : 'image';
274 }
275 if ( isset( $raw['streetviewdata'] ) || ! empty( $raw['streetviewurl'] ) ) {
276 return 'street-view';
277 }
278 if ( ! empty( $raw['vidid'] ) || ! empty( $raw['vidurl'] ) ) {
279 return 'video';
280 }
281 return 'image';
282 }
283
284 /**
285 * Extract pro advanced-control fields from raw panodata for the API response.
286 * tourLayout is stored as an array by pro but the React store uses a plain string.
287 */
288 protected function advanced_settings_to_api( array $raw ): array {
289 $b = static function ( $val, $default ) {
290 if ( is_bool( $val ) ) return $val;
291 return filter_var( $val ?? $default, FILTER_VALIDATE_BOOLEAN );
292 };
293
294 $tour_layout_raw = $raw['tourLayout'] ?? 'default';
295 $tour_layout = is_array( $tour_layout_raw )
296 ? ( $tour_layout_raw['layout'] ?? 'default' )
297 : (string) $tour_layout_raw;
298
299 // globalzoom is on when any of hfov/maxHfov/minHfov is set.
300 $globalzoom = ( ! empty( $raw['hfov'] ) || ! empty( $raw['maxHfov'] ) || ! empty( $raw['minHfov'] ) );
301
302 return [
303 'tourLayout' => $tour_layout,
304 'layout_icon_bg_color' => (string) ( $raw['layout_icon_bg_color'] ?? '#5a536e' ),
305 'layout_icon_color' => (string) ( $raw['layout_icon_color'] ?? '#ffffff' ),
306 'diskeyboard' => ! in_array( $raw['diskeyboard'] ?? 'off', [ 'on', 'true', true ], true ),
307 'keyboardzoom' => $b( $raw['keyboardzoom'] ?? null, true ),
308 'draggable' => $b( $raw['draggable'] ?? null, true ),
309 'mouseZoom' => $b( $raw['mouseZoom'] ?? null, true ),
310 'gyro' => $b( $raw['gyro'] ?? null, false ),
311 'deviceorientationcontrol' => $b( $raw['deviceorientationcontrol']?? null, false ),
312 'compass' => $b( $raw['compass'] ?? null, false ),
313 'vrgallery' => $b( $raw['vrgallery'] ?? null, false ),
314 'vrgallery_title' => $b( $raw['vrgallery_title'] ?? null, false ),
315 'vrgallery_icon_size' => $b( $raw['vrgallery_icon_size'] ?? null, false ),
316 'vrgallery_display' => $b( $raw['vrgallery_display'] ?? null, false ),
317 'scene_navigation' => $b( $raw['scene_navigation'] ?? null, false ),
318 'scene_navigation_content_type' => (string) ( $raw['scene_navigation_content_type'] ?? 'scene_id' ),
319 'sceneAnimation' => $b( $raw['sceneAnimation'] ?? null, false ),
320 'sceneAnimationName' => (string) ( $raw['sceneAnimationName'] ?? 'none' ),
321 'sceneAnimationTransitionDuration'=> (string) ( $raw['sceneAnimationTransitionDuration'] ?? '500' ),
322 'sceneAnimationTransitionDelay' => (string) ( $raw['sceneAnimationTransitionDelay'] ?? '0' ),
323 'bg_music' => $b( $raw['bg_music'] ?? null, false ),
324 'bg_music_url' => (string) ( $raw['bg_music_url'] ?? '' ),
325 'autoplay_bg_music' => $b( $raw['autoplay_bg_music'] ?? null, false ),
326 'loop_bg_music' => $b( $raw['loop_bg_music'] ?? null, false ),
327 'explainerSwitch' => $b( $raw['explainerSwitch'] ?? null, false ),
328 'explainerContent' => (string) ( $raw['explainerContent'] ?? '' ),
329 'cpLogoSwitch' => $b( $raw['cpLogoSwitch'] ?? null, false ),
330 'cpLogoImg' => (string) ( $raw['cpLogoImg'] ?? '' ),
331 'cpLogoContent' => (string) ( $raw['cpLogoContent'] ?? '' ),
332 'globalzoom' => $globalzoom,
333 'hfov' => isset( $raw['hfov'] ) && $raw['hfov'] !== '' ? (int) $raw['hfov'] : null,
334 'maxHfov' => isset( $raw['maxHfov'] ) && $raw['maxHfov'] !== '' ? (int) $raw['maxHfov'] : null,
335 'minHfov' => isset( $raw['minHfov'] ) && $raw['minHfov'] !== '' ? (int) $raw['minHfov'] : null,
336 'genericform' => ( ( $raw['genericform'] ?? 'off' ) === 'on' ),
337 'genericformshortcode' => (string) ( $raw['genericformshortcode'] ?? '' ),
338 'genericformicon' => (string) ( $raw['genericformicon'] ?? 'fab fa-wpforms' ),
339 'genericformiconcolor' => (string) ( $raw['genericformiconcolor'] ?? '#f7fffb' ),
340 'calltoaction' => ( ( $raw['calltoaction'] ?? 'off' ) === 'on' ),
341 'buttontext' => (string) ( $raw['buttontext'] ?? 'Click Here' ),
342 'buttonurl' => (string) ( $raw['buttonurl'] ?? '' ),
343 'button_configuration' => $this->button_configuration_to_api( $raw['button_configuration'] ?? [] ),
344 'customcss_enable' => ( ( $raw['customcss_enable'] ?? 'off' ) === 'on' ),
345 'customcss' => (string) ( $raw['customcss'] ?? '' ),
346 'customcontrol' => $this->customcontrol_to_api( $raw['customcontrol'] ?? [] ),
347 ];
348 }
349
350 private function customcontrol_to_api( $raw_ctrl ): array {
351 if ( ! is_array( $raw_ctrl ) ) {
352 $raw_ctrl = [];
353 }
354 $defaults = [
355 'panupSwitch' => 'off', 'panupColor' => '#f7fffb', 'panupIcon' => 'fas fa-angle-up',
356 'panDownSwitch' => 'off', 'panDownColor' => '#f7fffb', 'panDownIcon' => 'fas fa-angle-down',
357 'panLeftSwitch' => 'off', 'panLeftColor' => '#f7fffb', 'panLeftIcon' => 'fas fa-angle-left',
358 'panRightSwitch' => 'off', 'panRightColor' => '#f7fffb', 'panRightIcon' => 'fas fa-angle-right',
359 'panZoomInSwitch' => 'off', 'panZoomInColor' => '#f7fffb', 'panZoomInIcon' => 'fas fa-plus-circle',
360 'panZoomOutSwitch' => 'off', 'panZoomOutColor' => '#f7fffb', 'panZoomOutIcon' => 'fas fa-minus-circle',
361 'panFullscreenSwitch' => 'off', 'panFullscreenColor' => '#f7fffb', 'panFullscreenIcon' => 'fas fa-expand',
362 'gyroscopeSwitch' => 'off', 'gyroscopeColor' => '#f7fffb', 'gyroscopeIcon' => 'fas fa-dot-circle',
363 'backToHomeSwitch' => 'off', 'backToHomeColor' => '#f7fffb', 'backToHomeIcon' => 'fas fa-home',
364 'explainerColor' => '#f7fffb', 'explainerIcon' => 'fas fa-video',
365 ];
366 return array_merge( $defaults, array_intersect_key( $raw_ctrl, $defaults ) );
367 }
368
369 private function button_configuration_to_api( $raw_config ): array {
370 if ( ! is_array( $raw_config ) ) {
371 $raw_config = [];
372 }
373
374 $defaults = [
375 'button_open_new_tab' => 'off',
376 'button_background_color' => '#201cfe',
377 'button_font_color' => '#ffffff',
378 'button_font_size' => '14',
379 'button_font_weight' => '400',
380 'button_line_height' => '1',
381 'button_text_decoration' => 'none',
382 'button_transform' => 'none',
383 'button_alignment' => 'left',
384 'button_text_style' => 'normal',
385 'button_letter_spacing' => '1',
386 'button_word_spacing' => '0',
387 'button_border_width' => '1',
388 'button_border_style' => 'solid',
389 'button_border_color' => '#201cfe',
390 'button_border_radius' => '6',
391 'button_pt' => '10',
392 'button_pr' => '15',
393 'button_pb' => '10',
394 'button_pl' => '15',
395 ];
396 $config = array_merge( $defaults, array_intersect_key( $raw_config, $defaults ) );
397
398 $config['button_open_new_tab'] = in_array(
399 $config['button_open_new_tab'],
400 [ true, 1, '1', 'on' ],
401 true
402 ) ? 'on' : 'off';
403
404 $allowed_values = [
405 'button_font_weight' => [ '400', '500', '600', '700', '800', '900' ],
406 'button_text_decoration' => [ 'none', 'underline', 'overline', 'line-through' ],
407 'button_transform' => [ 'none', 'uppercase', 'lowercase', 'capitalize' ],
408 'button_alignment' => [ 'left', 'right', 'center', 'justified' ],
409 'button_text_style' => [ 'normal', 'italic', 'oblique' ],
410 'button_border_style' => [ 'solid', 'dashed', 'dotted', 'double', 'none' ],
411 ];
412 foreach ( $allowed_values as $key => $allowed ) {
413 $value = (string) $config[ $key ];
414 $config[ $key ] = in_array( $value, $allowed, true ) ? $value : $defaults[ $key ];
415 }
416
417 foreach ( [ 'button_background_color', 'button_font_color', 'button_border_color' ] as $key ) {
418 $value = (string) $config[ $key ];
419 $config[ $key ] = preg_match( '/^#[0-9a-fA-F]{6}$/', $value ) ? strtolower( $value ) : $defaults[ $key ];
420 }
421
422 foreach ( [
423 'button_font_size', 'button_line_height', 'button_letter_spacing',
424 'button_word_spacing', 'button_border_width', 'button_border_radius',
425 'button_pt', 'button_pr', 'button_pb', 'button_pl',
426 ] as $key ) {
427 $value = $config[ $key ];
428 $config[ $key ] = is_numeric( $value ) && (float) $value >= 0
429 ? (string) $value
430 : $defaults[ $key ];
431 }
432
433 return $config;
434 }
435
436 protected function scenes_to_api( array $scene_list ): array {
437 $n = static function ( $val, $cast ) {
438 if ( ! isset( $val ) || $val === '' ) return null;
439 return $cast === 'int' ? (int) $val : (float) $val;
440 };
441
442 $scenes = [];
443 foreach ( $scene_list as $scene ) {
444 $scenes[] = [
445 // Free fields
446 'id' => $scene['scene-id'] ?? '',
447 'name' => $scene['scene-ititle'] ?? '',
448 'type' => $scene['scene-type'] ?? 'equirectangular',
449 'imageUrl' => $scene['scene-attachment-url'] ?? '',
450 'isDefault' => ( $scene['dscene'] ?? 'off' ) === 'on',
451 'hotspots' => $this->hotspots_to_api( $scene['hotspot-list'] ?? [] ),
452 // Pro: metadata
453 'author' => $scene['scene-author'] ?? '',
454 'authorUrl' => $scene['scene-author-url'] ?? '',
455 'vaov' => $n( $scene['scene-vaov'] ?? null, 'int' ),
456 'haov' => $n( $scene['scene-haov'] ?? null, 'int' ),
457 'verticalOffset' => $n( $scene['scene-vertical-offset'] ?? null, 'float' ),
458 // Pro: default face orientation
459 'defaultFace' => $scene['ptyscene'] ?? 'off',
460 'pitch' => $n( $scene['scene-pitch'] ?? null, 'float' ),
461 'yaw' => $n( $scene['scene-yaw'] ?? null, 'float' ),
462 // Pro: vertical drag limits
463 'limitVertical' => $scene['cvgscene'] ?? 'off',
464 'maxPitch' => $n( $scene['scene-maxpitch'] ?? null, 'float' ),
465 'minPitch' => $n( $scene['scene-minpitch'] ?? null, 'float' ),
466 // Pro: horizontal drag limits
467 'limitHorizontal' => $scene['chgscene'] ?? 'off',
468 'maxYaw' => $n( $scene['scene-maxyaw'] ?? null, 'float' ),
469 'minYaw' => $n( $scene['scene-minyaw'] ?? null, 'float' ),
470 // Pro: per-scene zoom override
471 'customZoom' => $scene['czscene'] ?? 'off',
472 'zoom' => $n( $scene['scene-zoom'] ?? null, 'int' ),
473 'maxZoom' => $n( $scene['scene-maxzoom'] ?? null, 'int' ),
474 'minZoom' => $n( $scene['scene-minzoom'] ?? null, 'int' ),
475 // Pro: cubemap faces (6-element array, null when not set)
476 'cubemapFaces' => [
477 $scene['scene-attachment-url-face0'] ?? null,
478 $scene['scene-attachment-url-face1'] ?? null,
479 $scene['scene-attachment-url-face2'] ?? null,
480 $scene['scene-attachment-url-face3'] ?? null,
481 $scene['scene-attachment-url-face4'] ?? null,
482 $scene['scene-attachment-url-face5'] ?? null,
483 ],
484 ];
485 }
486 // Older tours did not store whether a hotspot entry point inherited
487 // the target scene face or was edited manually. Keep those links in
488 // inherit mode: only edits made through the new Navigation Entry Point
489 // controls explicitly record the custom mode.
490 foreach ( $scenes as &$scene ) {
491 foreach ( $scene['hotspots'] as &$hotspot ) {
492 if ( in_array( $hotspot['sceneEntryPointMode'] ?? '', [ 'inherit', 'custom' ], true ) ) {
493 continue;
494 }
495 $hotspot['sceneEntryPointMode'] = 'inherit';
496 }
497 unset( $hotspot );
498 }
499 unset( $scene );
500
501 return $scenes;
502 }
503
504 protected function scenes_from_api( array $scenes, string $default_scene_id = '', bool $show_scene_info = true ): array {
505 $scene_list = [];
506 $index = 1;
507 foreach ( $scenes as $scene ) {
508 $scene_id = $scene['id'] ?? '';
509 $faces = $scene['cubemapFaces'] ?? [];
510 $scene_data = [
511 // Free fields
512 'scene-id' => $scene_id,
513 'scene-ititle' => $scene['name'] ?? '',
514 'scene-type' => $this->is_pro && ( $scene['type'] ?? '' ) === 'cubemap' ? 'cubemap' : 'equirectangular',
515 'scene-attachment-url' => $scene['imageUrl'] ?? '',
516 'scene-show-info' => $show_scene_info ? 'on' : 'off',
517 'hotspot-list' => $this->hotspots_from_api( $scene['hotspots'] ?? [] ),
518 'dscene' => ( $default_scene_id !== '' && $scene_id === $default_scene_id ) ? 'on' : 'off',
519 ];
520
521 if ( $this->is_pro ) {
522 $scene_data = array_merge( $scene_data, [
523 // Pro: metadata
524 'scene-author' => $scene['author'] ?? '',
525 'scene-author-url' => $scene['authorUrl'] ?? '',
526 'scene-vaov' => $scene['vaov'] ?? '',
527 'scene-haov' => $scene['haov'] ?? '',
528 'scene-vertical-offset' => $scene['verticalOffset'] ?? '',
529 // Pro: default face orientation
530 'ptyscene' => $scene['defaultFace'] ?? 'off',
531 'scene-pitch' => $scene['pitch'] ?? '',
532 'scene-yaw' => $scene['yaw'] ?? '',
533 // Pro: vertical drag limits
534 'cvgscene' => $scene['limitVertical'] ?? 'off',
535 'scene-maxpitch' => $scene['maxPitch'] ?? '',
536 'scene-minpitch' => $scene['minPitch'] ?? '',
537 // Pro: horizontal drag limits
538 'chgscene' => $scene['limitHorizontal'] ?? 'off',
539 'scene-maxyaw' => $scene['maxYaw'] ?? '',
540 'scene-minyaw' => $scene['minYaw'] ?? '',
541 // Pro: per-scene zoom override
542 'czscene' => $scene['customZoom'] ?? 'off',
543 'scene-zoom' => $scene['zoom'] ?? '',
544 'scene-maxzoom' => $scene['maxZoom'] ?? '',
545 'scene-minzoom' => $scene['minZoom'] ?? '',
546 // Pro: cubemap faces
547 'scene-attachment-url-face0' => $faces[0] ?? '',
548 'scene-attachment-url-face1' => $faces[1] ?? '',
549 'scene-attachment-url-face2' => $faces[2] ?? '',
550 'scene-attachment-url-face3' => $faces[3] ?? '',
551 'scene-attachment-url-face4' => $faces[4] ?? '',
552 'scene-attachment-url-face5' => $faces[5] ?? '',
553 ] );
554 }
555
556 $scene_list[ $index ] = $scene_data;
557 $index++;
558 }
559 return $scene_list;
560 }
561
562 protected function hotspots_to_api( array $hotspot_list ): array {
563 $hotspots = [];
564 foreach ( $hotspot_list as $hotspot ) {
565 if ( empty( $hotspot['hotspot-title'] ) && empty( $hotspot['hotspot-type'] ) ) {
566 continue;
567 }
568 $scene_pitch = isset( $hotspot['hotspot-scene-pitch'] ) && $hotspot['hotspot-scene-pitch'] !== ''
569 ? (float) $hotspot['hotspot-scene-pitch']
570 : null;
571 $scene_yaw = isset( $hotspot['hotspot-scene-yaw'] ) && $hotspot['hotspot-scene-yaw'] !== ''
572 ? (float) $hotspot['hotspot-scene-yaw']
573 : null;
574 $product_id = isset( $hotspot['hotspot-product-id'] ) ? (string) $hotspot['hotspot-product-id'] : '';
575 $product_name = '';
576 if ( ! empty( $product_id ) && function_exists( 'wc_get_product' ) ) {
577 $product_obj = wc_get_product( $product_id );
578 if ( is_object( $product_obj ) ) {
579 $product_name = $product_obj->get_formatted_name();
580 }
581 }
582
583 $hotspots[] = [
584 'id' => $hotspot['hotspot-id'] ?? wp_generate_uuid4(),
585 'type' => $hotspot['hotspot-type'] ?? 'info',
586 'pitch' => isset( $hotspot['hotspot-pitch'] ) ? (float) $hotspot['hotspot-pitch'] : 0,
587 'yaw' => isset( $hotspot['hotspot-yaw'] ) ? (float) $hotspot['hotspot-yaw'] : 0,
588 'text' => $hotspot['hotspot-title'] ?? '',
589 'content' => $hotspot['hotspot-content'] ?? '',
590 'url' => $hotspot['hotspot-url'] ?? '',
591 'urlOpen' => $hotspot['hotspot-url-open'] ?? 'off',
592 'hover' => $hotspot['hotspot-hover'] ?? '',
593 'targetSceneId' => $hotspot['hotspot-scene'] ?? '',
594 'customClass' => $hotspot['hotspot-customclass'] ?? '',
595 'fluentFormId' => isset( $hotspot['fluent-form-id'] ) ? (string) $hotspot['fluent-form-id'] : '',
596 'productId' => $product_id,
597 'productName' => $product_name,
598 // Pro styling fields
599 'iconClass' => ( ( $hotspot['hotspot-customclass-pro'] ?? '' ) === 'none' || ( $hotspot['hotspot-customclass-pro'] ?? '' ) === '' ) ? '' : $hotspot['hotspot-customclass-pro'],
600 'iconBgColor' => $hotspot['hotspot-customclass-color-icon-value'] ?? '#00b4ff',
601 'iconColor' => $hotspot['hotspot-custom-icon-color-value'] ?? '#ffffff',
602 'blink' => $hotspot['hotspot-blink'] ?? 'on',
603 'shape' => $hotspot['hotspot-shape'] ?? 'round',
604 'border' => $hotspot['hotspot-border'] ?? 'off',
605 'borderWidth' => $hotspot['hotspot-border-width'] ?? '1',
606 'borderStyle' => $hotspot['hotspot-border-style'] ?? 'none',
607 'borderColor' => $hotspot['hotspot-border-color'] ?? '#00b4ff',
608 // Pro navigation entry point fields
609 'scenePitch' => $scene_pitch,
610 'sceneYaw' => $scene_yaw,
611 'sceneEntryPointMode' => $hotspot['hotspot-scene-entry-point-mode'] ?? null,
612 ];
613 }
614 return $hotspots;
615 }
616
617 protected function hotspots_from_api( array $hotspots ): array {
618 $hotspot_list = [];
619 foreach ( $hotspots as $hotspot ) {
620 $hotspot_type = sanitize_key( $hotspot['type'] ?? 'info' );
621 $is_fluent_form = $hotspot_type === 'fluent_form';
622 $raw_content = (string) ( $hotspot['content'] ?? '' );
623 $raw_hover = (string) ( $hotspot['hover'] ?? '' );
624
625 // For fluent_form hotspots, content is dynamically rendered server-side from fluent-form-id.
626 // User-supplied content is never used and must not be saved, completely eliminating stored XSS.
627 if ( $is_fluent_form ) {
628 $content = '';
629 } else {
630 $content = function_exists( 'sanitize_content_preserve_styles' )
631 ? sanitize_content_preserve_styles( $raw_content, false )
632 : wp_kses_post( $raw_content );
633 }
634 $hover = function_exists( 'sanitize_content_preserve_styles' )
635 ? sanitize_content_preserve_styles( $raw_hover, false )
636 : wp_kses_post( $raw_hover );
637
638 $hs = [
639 'hotspot-id' => $hotspot['id'] ?? wp_generate_uuid4(),
640 'hotspot-type' => $hotspot_type,
641 'hotspot-pitch' => (string) ( $hotspot['pitch'] ?? 0 ),
642 'hotspot-yaw' => (string) ( $hotspot['yaw'] ?? 0 ),
643 'hotspot-title' => sanitize_text_field( $hotspot['text'] ?? '' ),
644 'hotspot-content' => $content,
645 'hotspot-url' => sanitize_text_field( $hotspot['url'] ?? '' ),
646 'hotspot-url-open' => ( $hotspot['urlOpen'] ?? 'off' ) === 'on' ? 'on' : 'off',
647 'hotspot-hover' => $hover,
648 'hotspot-scene' => sanitize_text_field( $hotspot['targetSceneId'] ?? '' ),
649 'hotspot-customclass' => sanitize_text_field( $hotspot['customClass'] ?? '' ),
650 'hotspot-scene-list' => 'none',
651 ];
652
653 if ( isset( $hotspot['fluentFormId'] ) ) {
654 $hs['fluent-form-id'] = (string) absint( $hotspot['fluentFormId'] );
655 } elseif ( isset( $hotspot['fluent-form-id'] ) ) {
656 $hs['fluent-form-id'] = (string) absint( $hotspot['fluent-form-id'] );
657 }
658
659 if ( isset( $hotspot['productId'] ) ) {
660 $hs['hotspot-product-id'] = sanitize_text_field( $hotspot['productId'] );
661 } elseif ( isset( $hotspot['hotspot-product-id'] ) ) {
662 $hs['hotspot-product-id'] = sanitize_text_field( $hotspot['hotspot-product-id'] );
663 }
664
665 if ( $this->is_pro ) {
666 $hs = array_merge( $hs, [
667 // Pro styling fields
668 'hotspot-customclass-pro' => !empty( $hotspot['iconClass'] ) ? $hotspot['iconClass'] : 'none',
669 'hotspot-customclass-color-icon-value' => $hotspot['iconBgColor'] ?? '#00b4ff',
670 'hotspot-custom-icon-color-value' => $hotspot['iconColor'] ?? '#ffffff',
671 'hotspot-blink' => $hotspot['blink'] ?? 'on',
672 'hotspot-shape' => $hotspot['shape'] ?? 'round',
673 'hotspot-border' => $hotspot['border'] ?? 'off',
674 'hotspot-border-width' => $hotspot['borderWidth'] ?? '1',
675 'hotspot-border-style' => $hotspot['borderStyle'] ?? 'none',
676 'hotspot-border-color' => $hotspot['borderColor'] ?? '#00b4ff',
677 'hotspot-scene-pitch' => ( isset( $hotspot['scenePitch'] ) && $hotspot['scenePitch'] !== null ) ? (string) $hotspot['scenePitch'] : '',
678 'hotspot-scene-yaw' => ( isset( $hotspot['sceneYaw'] ) && $hotspot['sceneYaw'] !== null ) ? (string) $hotspot['sceneYaw'] : '',
679 'hotspot-scene-entry-point-mode' => in_array( $hotspot['sceneEntryPointMode'] ?? '', [ 'inherit', 'custom' ], true )
680 ? $hotspot['sceneEntryPointMode']
681 : 'inherit',
682 ] );
683 }
684
685 $hotspot_list[] = $hs;
686 }
687 return $hotspot_list;
688 }
689 }
690