PluginProbe
WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress / 9.1.0
WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress v9.1.0
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 / Services / TourService.php

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

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