PluginProbe
WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress / 9.1.2
WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress v9.1.2
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.1.2, at src/Api/Services/TourService.php

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