PluginProbe
WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress / 9.0.1
WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress v9.0.1
9.1.2 9.1.1 9.1.0 9.0.3 9.0.2 9.0.1 9.0.0 8.5.79 8.5.78 8.5.77 8.5.76 8.5.75 8.5.74 8.5.73 8.5.72 8.5.71 8.5.70 8.5.69 8.5.68 8.5.35 8.5.36 8.5.37 8.5.38 8.5.39 8.5.4 All 221 releases
wpvr / src / Api / Services / TourService.php

TourService.php in WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress 9.0.1, at src/Api/Services/TourService.php

270 lines 11.8 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\Services;
4
5 use RexTheme\WPVR\Api\Contracts\RepositoryInterface;
6 use RexTheme\WPVR\Api\Contracts\TransformerInterface;
7
8 class TourService {
9
10 private RepositoryInterface $repository;
11 private TransformerInterface $transformer;
12 private bool $is_pro;
13
14 public function __construct( RepositoryInterface $repository, TransformerInterface $transformer, bool $is_pro = false ) {
15 $this->repository = $repository;
16 $this->transformer = $transformer;
17 $this->is_pro = $is_pro;
18 }
19
20 public function get( int $tour_id ): array {
21 $raw = $this->repository->find( $tour_id );
22 return $this->transformer->toApi( $raw );
23 }
24
25 public function update( int $tour_id, array $api_data ): bool {
26 // Read existing panodata first so we can preserve pro-managed fields
27 // that the free transformer does not handle.
28 $existing_raw = $this->repository->find( $tour_id );
29
30 // Convert the API (free) fields to raw panodata format.
31 $raw = $this->transformer->fromApi( $api_data );
32
33 if ( ! $this->is_pro ) {
34 $raw = $this->preserve_pro_data( $existing_raw, $raw );
35 }
36
37 // Merge: existing supplies fields unknown to the new editor; transformed
38 // values overwrite fields the current plan is allowed to manage.
39 if ( is_array( $existing_raw ) && ! empty( $existing_raw ) ) {
40 $raw = array_merge( $existing_raw, $raw );
41 }
42
43 // If the request carries proData (future React pro panels will send this),
44 // run the same filter the legacy AJAX stack uses so pro plugin saves its fields.
45 if ( $this->is_pro && ! empty( $api_data['proData'] ) && is_array( $api_data['proData'] ) ) {
46 // Snapshot floor plan keys before the pro filter runs.
47 // The pro filter reads floor plan from legacy $_POST-style fields (floor_list_data,
48 // wpvr_floor_plan_enabler, etc.) which are not present in proData when saving from the
49 // React editor. Without this, the filter overwrites our correctly-converted floor plan
50 // data with empty values.
51 $fp_keys = [
52 'floor_plan_tour_enabler', 'floor_plan_attachment_url', 'floor_plan_custom_color',
53 'floor_plan_pointer_position', 'floor_plan_data_list', 'floor_plan_direction_indicator',
54 'floorplan-enabled', 'floorplan-image', 'floorplan-compass', 'floorplan-compass-color',
55 ];
56 $fp_snapshot = [];
57 foreach ( $fp_keys as $k ) {
58 if ( array_key_exists( $k, $raw ) ) {
59 $fp_snapshot[ $k ] = $raw[ $k ];
60 }
61 }
62
63 $post_format = array_merge( $api_data['proData'], [ 'postid' => (string) $tour_id ] );
64 // React toggles are booleans, while the legacy Pro save filter and
65 // shortcode renderer require the exact strings "on" / "off".
66 // Without this conversion an enabled scene-navigation toggle is
67 // stored as "1", so the renderer's strict "on" check never passes.
68 if ( array_key_exists( 'scene_navigation', $post_format ) ) {
69 $scene_navigation = $post_format['scene_navigation'];
70 $post_format['scene_navigation'] = ( ! empty( $scene_navigation ) && $scene_navigation !== 'off' ) ? 'on' : 'off';
71 }
72 if ( array_key_exists( 'sceneAnimation', $post_format ) ) {
73 $scene_animation = $post_format['sceneAnimation'];
74 $post_format['sceneAnimation'] = ( ! empty( $scene_animation ) && $scene_animation !== 'off' ) ? 'on' : 'off';
75 }
76 if ( ( $post_format['sceneAnimationName'] ?? '' ) === 'fade' ) {
77 $post_format['sceneAnimationName'] = 'fade_in';
78 }
79 $advanced_control = is_array( $api_data['advancedControl'] ?? null ) ? $api_data['advancedControl'] : [];
80 $advanced_control = $this->normalize_advanced_control( $advanced_control );
81
82 // The legacy Pro filter expects the CTA toggle under callToAction and
83 // every button style as a flat POST field. The React editor keeps
84 // these values in advancedControl.button_configuration.
85 $post_format['callToAction'] = $advanced_control['calltoaction'] ?? ( $raw['calltoaction'] ?? 'off' );
86 $post_format['buttontext'] = $raw['buttontext'] ?? 'Click Here';
87 $post_format['buttonurl'] = $raw['buttonurl'] ?? '';
88 $button_configuration = is_array( $raw['button_configuration'] ?? null )
89 ? $raw['button_configuration']
90 : [];
91 foreach ( $button_configuration as $key => $value ) {
92 $post_format[ $key ] = $value;
93 }
94
95 $raw = apply_filters( 'prepare_scene_pano_array_with_pro_version', $raw, $post_format, $advanced_control );
96
97 // Restore floor plan keys that the pro filter may have blanked out because it could
98 // not find the legacy $_POST floor-plan fields in proData.
99 foreach ( $fp_snapshot as $k => $v ) {
100 $current = $raw[ $k ] ?? null;
101 $blanked = ( $current === null || $current === '' || $current === [] || $current === 'off' );
102 $had_val = ( $v !== null && $v !== '' && $v !== [] );
103 if ( $blanked && $had_val ) {
104 $raw[ $k ] = $v;
105 }
106 }
107 }
108
109 $saved = $this->repository->save( $tour_id, $raw );
110
111 if ( $saved ) {
112 do_action( 'rex_wpvr_tour_saved', $tour_id );
113 do_action( 'wpvr_pro_delete_orphaned_analytics_data', $tour_id );
114 }
115
116 return $saved;
117 }
118
119 /**
120 * Keep all stored Pro values during a Free-plan save while still allowing
121 * changes to the free scene and hotspot fields.
122 */
123 private function preserve_pro_data( array $existing, array $incoming ): array {
124 $protected_top_level = [
125 'tourLayout', 'layout_icon_bg_color', 'layout_icon_color',
126 'diskeyboard', 'keyboardzoom', 'draggable', 'mouseZoom', 'gyro',
127 'deviceorientationcontrol', 'compass', 'vrgallery', 'vrgallery_title',
128 'vrgallery_icon_size', 'vrgallery_display', 'scene_navigation',
129 'scene_navigation_content_type', 'sceneAnimation', 'sceneAnimationName',
130 'sceneAnimationTransitionDuration', 'sceneAnimationTransitionDelay',
131 'bg_music', 'bg_music_url', 'autoplay_bg_music', 'loop_bg_music',
132 'explainerSwitch', 'explainerContent', 'cpLogoSwitch', 'cpLogoImg',
133 'cpLogoContent', 'hfov', 'maxHfov', 'minHfov', 'genericform',
134 'genericformshortcode', 'genericformicon', 'genericformiconcolor',
135 'calltoaction', 'buttontext', 'buttonurl', 'button_configuration',
136 'customcss_enable', 'customcss', 'customcontrol',
137 'floorplan-enabled', 'floorplan-image', 'floorplan-compass',
138 'floorplan-compass-color', 'floor_plan_tour_enabler',
139 'floor_plan_attachment_url', 'floor_plan_custom_color',
140 'floor_plan_direction_indicator', 'floor_plan_pointer_position',
141 'floor_plan_data_list', 'bg_tour_enabler', 'bg_tour_title',
142 'bg_tour_subtitle', 'video-autoplay', 'video-loop',
143 ];
144
145 foreach ( $protected_top_level as $key ) {
146 unset( $incoming[ $key ] );
147 }
148
149 if ( ( $existing['tour-type'] ?? '' ) === 'street-view' || isset( $existing['streetviewdata'] ) ) {
150 foreach ( [ 'tour-type', 'streetview', 'streetviewurl', 'streetviewdata' ] as $key ) {
151 unset( $incoming[ $key ] );
152 }
153 }
154
155 $existing_scenes = $existing['panodata']['scene-list'] ?? [];
156 $incoming_scenes = $incoming['panodata']['scene-list'] ?? [];
157 $scene_map = [];
158 foreach ( $existing_scenes as $scene ) {
159 if ( is_array( $scene ) && ! empty( $scene['scene-id'] ) ) {
160 $scene_map[ (string) $scene['scene-id'] ] = $scene;
161 }
162 }
163
164 foreach ( $incoming_scenes as $index => $scene ) {
165 if ( ! is_array( $scene ) ) {
166 continue;
167 }
168
169 $scene_id = (string) ( $scene['scene-id'] ?? '' );
170 $existing_scene = $scene_map[ $scene_id ] ?? [];
171 if ( empty( $existing_scene ) ) {
172 continue;
173 }
174
175 $existing_hotspots = $existing_scene['hotspot-list'] ?? [];
176 $incoming_hotspots = $scene['hotspot-list'] ?? [];
177 $hotspot_map = [];
178 foreach ( $existing_hotspots as $hotspot ) {
179 if ( is_array( $hotspot ) && ! empty( $hotspot['hotspot-id'] ) ) {
180 $hotspot_map[ (string) $hotspot['hotspot-id'] ] = $hotspot;
181 }
182 }
183
184 foreach ( $incoming_hotspots as $hotspot_index => $hotspot ) {
185 if ( ! is_array( $hotspot ) ) {
186 continue;
187 }
188 $hotspot_id = (string) ( $hotspot['hotspot-id'] ?? '' );
189 if ( isset( $hotspot_map[ $hotspot_id ] ) ) {
190 $incoming_hotspots[ $hotspot_index ] = array_merge( $hotspot_map[ $hotspot_id ], $hotspot );
191 }
192 }
193
194 $scene['hotspot-list'] = $incoming_hotspots;
195 $merged_scene = array_merge( $existing_scene, $scene );
196
197 if ( ( $existing_scene['scene-type'] ?? '' ) === 'cubemap' ) {
198 $merged_scene['scene-type'] = 'cubemap';
199 }
200
201 $incoming_scenes[ $index ] = $merged_scene;
202 }
203
204 $incoming['panodata'] = array_merge(
205 is_array( $existing['panodata'] ?? null ) ? $existing['panodata'] : [],
206 is_array( $incoming['panodata'] ?? null ) ? $incoming['panodata'] : [],
207 [ 'scene-list' => $incoming_scenes ]
208 );
209
210 return $incoming;
211 }
212
213 /**
214 * Normalize React boolean values to the exact format legacy PHP stores them.
215 *
216 * set_pro_checkbox_value / sanitize_text_field fields → 'on'/'off' string.
217 * set_checkbox_value / set_checkbox_on_value fields → PHP bool (already correct).
218 */
219 private function normalize_advanced_control( array $data ): array {
220 $on_off_keys = [
221 'mouseZoom', 'draggable', 'diskeyboard',
222 'bg_music', 'autoplay_bg_music', 'loop_bg_music',
223 'cpLogoSwitch', 'explainerSwitch',
224 'genericform', 'calltoaction', 'customcss_enable',
225 ];
226 foreach ( $on_off_keys as $key ) {
227 if ( array_key_exists( $key, $data ) ) {
228 $v = $data[ $key ];
229 $data[ $key ] = ( ! empty( $v ) && $v !== 'off' ) ? 'on' : 'off';
230 }
231 }
232 return $data;
233 }
234
235 /**
236 * Create a wpvr_item post. Returns new post ID or WP_Error.
237 *
238 * @param array $args { title?: string, tourType?: string }
239 * @return int|\WP_Error
240 */
241 public function create( array $args = [] ) {
242 $title = ! empty( $args['title'] )
243 ? sanitize_text_field( $args['title'] )
244 : __( 'New Tour', 'wpvr' );
245
246 $allowed = [ 'image', 'video', 'street-view' ];
247 $tour_type = in_array( $args['tourType'] ?? 'image', $allowed, true )
248 ? ( $args['tourType'] ?? 'image' )
249 : 'image';
250
251 if ( ! $this->is_pro && $tour_type === 'street-view' ) {
252 $tour_type = 'image';
253 }
254
255 $post_id = wp_insert_post( [
256 'post_type' => 'wpvr_item',
257 'post_status' => 'draft',
258 'post_title' => $title,
259 ], true );
260
261 if ( is_wp_error( $post_id ) ) {
262 return $post_id;
263 }
264
265 $this->repository->save( $post_id, [ 'tour-type' => $tour_type ] );
266
267 return $post_id;
268 }
269 }
270