PluginProbe
User Profile Builder – Beautiful User Registration Forms, User Profiles & User Role Editor / 4.0.1
User Profile Builder – Beautiful User Registration Forms, User Profiles & User Role Editor v4.0.1
4.0.3 4.0.2 4.0.1 4.0.0 3.16.6 3.16.5 3.16.4 3.16.3 3.16.2 3.16.1 3.16.0 3.15.9 3.9.9 3.9.5 3.9.6 3.9.7 3.9.8 1.1.7 1.1.8 1.1.9 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 All 341 releases
profile-builder / form-builder / integrations / multi-step-forms.php

multi-step-forms.php in User Profile Builder – Beautiful User Registration Forms, User Profiles & User Role Editor 4.0.1, at form-builder/integrations/multi-step-forms.php

421 lines 17.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Multi-Step Forms ↔ Form Builder bridge.
4 *
5 * Harvests step-break blocks into `wppb_msf_break_points` on save, reinjects them
6 * on REST read, and mirrors pagination/tabs/tab-titles via virtual `wppb_fb_msf_*` keys.
7 * All forms write post meta; option-level data migrates once at the bottom of this file.
8 */
9
10 if ( ! defined( 'ABSPATH' ) ) exit;
11
12 // Loaded by wppb_fb_load_addon_integrations(); do not include directly.
13
14 // Keep the Step Break structural block in the inserter while MSF is active.
15 add_filter( 'wppb_fb_enabled_structural_blocks', function ( $blocks ) {
16 $blocks[] = 'profile-builder/msf-step-break';
17 return $blocks;
18 } );
19
20 /**
21 * Per-save state by post_id: break-point id set + last flattened top-level field id.
22 */
23 function &wppb_msf_fb_state() {
24 static $state = array();
25 return $state;
26 }
27
28 add_action( 'wppb_fb_field_flattened', 'wppb_msf_fb_track_last_field', 10, 4 );
29 function wppb_msf_fb_track_last_field( $field_id, $attrs, $field_type, $context ) {
30 if ( ! empty( $context['is_subfield'] ) ) return;
31 if ( empty( $context['post_id'] ) ) return;
32
33 $state = &wppb_msf_fb_state();
34 $post_id = (int) $context['post_id'];
35 if ( ! isset( $state[ $post_id ] ) ) {
36 $state[ $post_id ] = array( 'points' => array(), 'last_id' => 0 );
37 }
38 $state[ $post_id ]['last_id'] = (int) $field_id;
39 }
40
41 /** Mark the preceding top-level field when a step-break is walked. */
42 add_action( 'wppb_fb_pb_block_walked', 'wppb_msf_fb_detect_step_break', 10, 2 );
43 function wppb_msf_fb_detect_step_break( $block, $context ) {
44 if ( empty( $block['blockName'] ) || $block['blockName'] !== 'profile-builder/msf-step-break' ) return;
45 if ( ! empty( $context['is_subfield'] ) ) return;
46 if ( empty( $context['post_id'] ) ) return;
47
48 $state = &wppb_msf_fb_state();
49 $post_id = (int) $context['post_id'];
50 if ( ! isset( $state[ $post_id ] ) ) {
51 $state[ $post_id ] = array( 'points' => array(), 'last_id' => 0 );
52 }
53
54 // Leading step-break with no preceding field — no-op.
55 if ( $state[ $post_id ]['last_id'] > 0 ) {
56 $id = $state[ $post_id ]['last_id'];
57 $state[ $post_id ]['points'][ $id ] = $id;
58 }
59 }
60
61 /**
62 * Flush break-points to post meta. Strip the last field id — legacy forbids a break on the final field.
63 */
64 add_action( 'wppb_fb_post_project', 'wppb_msf_fb_flush_break_points', 10, 3 );
65 function wppb_msf_fb_flush_break_points( $post_id, $post, $context ) {
66 $state = &wppb_msf_fb_state();
67 $entry = isset( $state[ $post_id ] ) ? $state[ $post_id ] : array( 'points' => array(), 'last_id' => 0 );
68 $points = $entry['points'];
69
70 if ( ! empty( $points ) ) {
71 $meta_key = $post->post_type === 'wppb-rf-cpt' ? 'wppb_rf_fields' : 'wppb_epf_fields';
72 $ordered = get_post_meta( $post_id, $meta_key, true );
73 if ( is_array( $ordered ) && ! empty( $ordered ) ) {
74 $last = end( $ordered );
75 if ( is_array( $last ) && isset( $last['id'] ) ) {
76 unset( $points[ (int) $last['id'] ] );
77 }
78 }
79 }
80
81 if ( empty( $points ) ) {
82 delete_post_meta( $post_id, 'wppb_msf_break_points' );
83 } else {
84 update_post_meta( $post_id, 'wppb_msf_break_points', $points );
85 }
86
87 unset( $state[ $post_id ] );
88 }
89
90 /** Inject msf-step-break blocks after fields listed in wppb_msf_break_points. */
91 add_filter( 'wppb_fb_form_blocks', 'wppb_msf_fb_inject_step_break_blocks', 10, 3 );
92 function wppb_msf_fb_inject_step_break_blocks( $blocks, $post_id, $post_type ) {
93 if ( ! in_array( $post_type, array( 'wppb-rf-cpt', 'wppb-epf-cpt' ), true ) ) return $blocks;
94 $bp = get_post_meta( $post_id, 'wppb_msf_break_points', true );
95 if ( ! is_array( $bp ) || empty( $bp ) ) return $blocks;
96
97 $separator = array(
98 'blockName' => 'profile-builder/msf-step-break',
99 'attrs' => array(),
100 'innerBlocks' => array(),
101 'innerHTML' => '',
102 'innerContent' => array(),
103 );
104
105 $out = array();
106 $last_index = count( $blocks ) - 1;
107 foreach ( $blocks as $i => $block ) {
108 $out[] = $block;
109 if ( $i === $last_index ) continue;
110 $id = isset( $block['attrs']['id'] ) ? (int) $block['attrs']['id'] : 0;
111 if ( $id > 0 && isset( $bp[ $id ] ) ) {
112 $out[] = $separator;
113 }
114 }
115 return $out;
116 }
117
118 /**
119 * Virtual REST keys → legacy storage. nested_key null means write the value to storage directly.
120 */
121 function wppb_msf_fb_virtual_keys() {
122 return array(
123 'wppb_fb_msf_pagination' => array( 'storage' => 'wppb_msf_post_options', 'nested_key' => 'msf-pagination' ),
124 'wppb_fb_msf_tabs' => array( 'storage' => 'wppb_msf_post_options', 'nested_key' => 'msf-tabs' ),
125 'wppb_fb_msf_tab_titles' => array( 'storage' => 'wppb_msf_tab_titles', 'nested_key' => null ),
126 );
127 }
128
129 /** Normalize to 'yes'/'no' — legacy renderer compares against the literal 'yes'. */
130
131 function wppb_msf_fb_sanitize_yes_no( $value ) {
132 return $value === 'yes' ? 'yes' : 'no';
133 }
134
135 /** Sanitize tab-titles array (dense indices). */
136
137 function wppb_msf_fb_sanitize_tab_titles( $value ) {
138 if ( is_string( $value ) ) {
139 $decoded = json_decode( $value, true );
140 $value = is_array( $decoded ) ? $decoded : array();
141 }
142 if ( ! is_array( $value ) ) return array();
143
144 $out = array();
145 foreach ( $value as $i => $title ) {
146 $out[ (int) $i ] = is_string( $title ) ? sanitize_text_field( $title ) : '';
147 }
148 return $out;
149 }
150
151 add_action( 'init', 'wppb_msf_fb_register_virtual_meta' );
152 function wppb_msf_fb_register_virtual_meta() {
153 foreach ( array( 'wppb-rf-cpt', 'wppb-epf-cpt' ) as $post_type ) {
154 // manage_options write gate — mirror folds into unvalidated legacy storage.
155 register_post_meta( $post_type, 'wppb_fb_msf_pagination', array(
156 'show_in_rest' => true, 'single' => true, 'type' => 'string',
157 'sanitize_callback' => 'wppb_msf_fb_sanitize_yes_no',
158 'auth_callback' => 'wppb_fb_virtual_meta_auth',
159 ) );
160 register_post_meta( $post_type, 'wppb_fb_msf_tabs', array(
161 'show_in_rest' => true, 'single' => true, 'type' => 'string',
162 'sanitize_callback' => 'wppb_msf_fb_sanitize_yes_no',
163 'auth_callback' => 'wppb_fb_virtual_meta_auth',
164 ) );
165 register_post_meta( $post_type, 'wppb_fb_msf_tab_titles', array(
166 'show_in_rest' => array(
167 'schema' => array(
168 'type' => 'array',
169 'items' => array( 'type' => 'string' ),
170 ),
171 ),
172 'single' => true,
173 'type' => 'array',
174 'sanitize_callback' => 'wppb_msf_fb_sanitize_tab_titles',
175 'auth_callback' => 'wppb_fb_virtual_meta_auth',
176 ) );
177 }
178 }
179
180 /** Fold flat wppb_fb_msf_* writes into legacy post meta and delete the flat row. */
181 add_action( 'added_post_meta', 'wppb_msf_fb_mirror_settings', 10, 4 );
182 add_action( 'updated_post_meta', 'wppb_msf_fb_mirror_settings', 10, 4 );
183 function wppb_msf_fb_mirror_settings( $meta_id, $post_id, $meta_key, $meta_value ) {
184 $map = wppb_msf_fb_virtual_keys();
185 if ( ! isset( $map[ $meta_key ] ) ) return;
186 if ( ! in_array( get_post_type( $post_id ), array( 'wppb-rf-cpt', 'wppb-epf-cpt' ), true ) ) return;
187
188 $route = $map[ $meta_key ];
189
190 if ( $route['nested_key'] === null ) {
191 // Dense panel array → sparse legacy storage (blank steps must be absent for isset()-based defaults).
192 $value = is_string( $meta_value ) ? json_decode( $meta_value, true ) : $meta_value;
193 $value = is_array( $value ) ? $value : array();
194
195 $titles = array();
196 foreach ( $value as $i => $title ) {
197 if ( is_string( $title ) && $title !== '' ) {
198 $titles[ (int) $i ] = sanitize_text_field( $title );
199 }
200 }
201
202 if ( empty( $titles ) ) {
203 delete_post_meta( $post_id, $route['storage'] );
204 } else {
205 update_post_meta( $post_id, $route['storage'], $titles );
206 }
207
208 if ( function_exists( 'wppb_icl_register_string' ) ) {
209 $form_type = get_post_type( $post_id ) === 'wppb-rf-cpt' ? 'register' : 'edit_profile';
210 foreach ( $titles as $i => $title ) {
211 wppb_icl_register_string( 'plugin profile-builder-pro', 'msf_' . $form_type . '_step_' . $i . '_tab_title_translation', $title );
212 }
213 }
214 } else {
215 $existing = get_post_meta( $post_id, $route['storage'], true );
216 if ( ! is_array( $existing ) ) $existing = array();
217 $existing[ $route['nested_key'] ] = is_string( $meta_value ) ? $meta_value : '';
218 update_post_meta( $post_id, $route['storage'], $existing );
219 }
220
221 delete_post_meta( $post_id, $meta_key );
222 }
223
224 /** Project legacy MSF post meta onto flat wppb_fb_msf_* REST keys. */
225 add_filter( 'rest_prepare_wppb-rf-cpt', 'wppb_msf_fb_inject_settings_into_response', 11, 3 );
226 add_filter( 'rest_prepare_wppb-epf-cpt', 'wppb_msf_fb_inject_settings_into_response', 11, 3 );
227 function wppb_msf_fb_inject_settings_into_response( $response, $post, $request ) {
228 if ( ! $response instanceof WP_REST_Response ) return $response;
229 $data = $response->get_data();
230 if ( ! isset( $data['meta'] ) || ! is_array( $data['meta'] ) ) {
231 $data['meta'] = array();
232 }
233
234 $post_id = (int) $post->ID;
235 $opts = get_post_meta( $post_id, 'wppb_msf_post_options', true );
236 $tab_titles = get_post_meta( $post_id, 'wppb_msf_tab_titles', true );
237
238 $opts = is_array( $opts ) ? $opts : array();
239 $tab_titles = is_array( $tab_titles ) ? $tab_titles : array();
240
241 // Sparse → dense (gaps → ''); do not array_values() — that would shift titles.
242 $dense = array();
243 if ( ! empty( $tab_titles ) ) {
244 $max = max( array_map( 'intval', array_keys( $tab_titles ) ) );
245 for ( $i = 0; $i <= $max; $i++ ) {
246 $dense[ $i ] = isset( $tab_titles[ $i ] ) ? (string) $tab_titles[ $i ] : '';
247 }
248 }
249
250 $data['meta']['wppb_fb_msf_pagination'] = isset( $opts['msf-pagination'] ) ? (string) $opts['msf-pagination'] : '';
251 $data['meta']['wppb_fb_msf_tabs'] = isset( $opts['msf-tabs'] ) ? (string) $opts['msf-tabs'] : '';
252 $data['meta']['wppb_fb_msf_tab_titles'] = $dense;
253
254 $response->set_data( $data );
255 return $response;
256 }
257
258 /** GET /wppb/v1/msf-state — post-save MSF state for editor reconcile. */
259 add_action( 'rest_api_init', 'wppb_msf_fb_register_rest_routes' );
260 function wppb_msf_fb_register_rest_routes() {
261 register_rest_route( 'wppb/v1', '/msf-state', array(
262 'methods' => 'GET',
263 'callback' => 'wppb_msf_fb_rest_state',
264 'permission_callback' => function () { return current_user_can( 'manage_options' ); },
265 'args' => array(
266 'post_id' => array( 'type' => 'integer', 'required' => true ),
267 ),
268 ) );
269 }
270
271 function wppb_msf_fb_rest_state( $request ) {
272 $post_id = (int) $request->get_param( 'post_id' );
273 if ( $post_id <= 0 ) return new WP_Error( 'invalid_post_id', 'Missing post_id', array( 'status' => 400 ) );
274 if ( ! in_array( get_post_type( $post_id ), array( 'wppb-rf-cpt', 'wppb-epf-cpt' ), true ) ) {
275 return new WP_Error( 'invalid_post_type', 'Not a Profile Builder form', array( 'status' => 400 ) );
276 }
277
278 $bp = get_post_meta( $post_id, 'wppb_msf_break_points', true );
279 $opts = get_post_meta( $post_id, 'wppb_msf_post_options', true );
280 $titles = get_post_meta( $post_id, 'wppb_msf_tab_titles', true );
281
282 $bp = is_array( $bp ) ? array_values( array_map( 'intval', $bp ) ) : array();
283 $opts = is_array( $opts ) ? $opts : array();
284 $titles = is_array( $titles ) ? array_values( $titles ) : array();
285
286 return rest_ensure_response( array(
287 'breakPoints' => $bp,
288 'pagination' => isset( $opts['msf-pagination'] ) ? (string) $opts['msf-pagination'] : '',
289 'tabs' => isset( $opts['msf-tabs'] ) ? (string) $opts['msf-tabs'] : '',
290 'tabTitles' => $titles,
291 ) );
292 }
293
294 /** Publish MSF REST root + nonce to the editor (priority 11, after the form-builder bundle). */
295 add_action( 'enqueue_block_editor_assets', 'wppb_msf_fb_enqueue_inline_bridge', 11 );
296 function wppb_msf_fb_enqueue_inline_bridge() {
297 $post_type = get_post_type();
298 if ( ! in_array( $post_type, array( 'wppb-rf-cpt', 'wppb-epf-cpt' ), true ) ) return;
299 if ( ! wp_script_is( 'wppb-form-editor-bundle', 'registered' ) ) return;
300
301 wp_add_inline_script(
302 'wppb-form-editor-bundle',
303 'window.wppbFb = window.wppbFb || {}; window.wppbFb.msf = ' . wp_json_encode( array(
304 'attributeName' => 'wppb-msf-break',
305 'restRoot' => esc_url_raw( rest_url( 'wppb/v1/msf-state' ) ),
306 'restNonce' => wp_create_nonce( 'wp_rest' ),
307 ) ) . ';',
308 'before'
309 );
310 }
311
312 /**
313 * Mirror option-level MSF state into default-form posts when the classic enable gate is 'yes'.
314 * Option → post meta only; options stay the classic-admin source of truth.
315 */
316 function wppb_msf_fb_mirror_options_to_default_forms() {
317 $defaults = get_option( 'wppb_default_form_ids', array() );
318 if ( ! is_array( $defaults ) || empty( $defaults ) ) return;
319
320 $opts = get_option( 'wppb_msf_options', array() );
321 $bp = get_option( 'wppb_msf_break_points', array() );
322 $titles = get_option( 'wppb_msf_tab_titles', array() );
323
324 $opts = is_array( $opts ) ? $opts : array();
325 $bp = is_array( $bp ) ? $bp : array();
326 $titles = is_array( $titles ) ? $titles : array();
327
328 $gate_map = array(
329 'register' => 'pb-default-register',
330 'edit_profile' => 'pb-default-edit-profile',
331 );
332
333 foreach ( $gate_map as $key => $gate ) {
334 if ( empty( $defaults[ $key ] ) ) continue;
335 if ( empty( $opts ) || ( isset( $opts[ $gate ] ) ? $opts[ $gate ] : 'no' ) !== 'yes' ) continue;
336
337 $post_id = (int) $defaults[ $key ];
338
339 if ( ! empty( $bp ) ) {
340 update_post_meta( $post_id, 'wppb_msf_break_points', $bp );
341 } else {
342 delete_post_meta( $post_id, 'wppb_msf_break_points' );
343 }
344
345 if ( ! empty( $titles ) ) {
346 update_post_meta( $post_id, 'wppb_msf_tab_titles', $titles );
347 } else {
348 delete_post_meta( $post_id, 'wppb_msf_tab_titles' );
349 }
350
351 update_post_meta( $post_id, 'wppb_msf_post_options', array(
352 'msf-pagination' => isset( $opts['msf-pagination'] ) ? $opts['msf-pagination'] : 'no',
353 'msf-tabs' => isset( $opts['msf-tabs'] ) ? $opts['msf-tabs'] : 'no',
354 ) );
355 }
356 }
357
358 /**
359 * One-time migration after default-form slots exist (`wppb_default_form_ids`, init:25).
360 */
361 add_action( 'init', 'wppb_msf_fb_migrate_to_post_meta', 25 );
362 function wppb_msf_fb_migrate_to_post_meta() {
363 if ( get_option( 'wppb_msf_fb_migrated' ) === '1' ) return;
364
365 $default_ids = get_option( 'wppb_default_form_ids', array() );
366 if ( ! is_array( $default_ids ) || empty( $default_ids['register'] ) || empty( $default_ids['edit_profile'] ) ) return;
367
368 wppb_msf_fb_mirror_options_to_default_forms();
369 update_option( 'wppb_msf_fb_migrated', '1' );
370 }
371
372 add_action( 'updated_option', 'wppb_msf_fb_option_changed', 10, 3 );
373 add_action( 'added_option', 'wppb_msf_fb_option_added', 10, 2 );
374
375 function wppb_msf_fb_option_changed( $option, $old_value, $new_value ) {
376 if ( ! in_array( $option, array( 'wppb_msf_options', 'wppb_msf_break_points', 'wppb_msf_tab_titles' ), true ) ) return;
377
378 // Tear down only on observed yes→off gate transition (not "gate isn't yes").
379 if ( $option === 'wppb_msf_options' ) {
380 wppb_msf_fb_mirror_gate_disables( $old_value, $new_value );
381 }
382
383 wppb_msf_fb_mirror_options_to_default_forms();
384 }
385
386 /** Clear mirrored MSF post meta when a classic enable gate goes from 'yes' to off. */
387
388 function wppb_msf_fb_mirror_gate_disables( $old_value, $new_value ) {
389 $defaults = get_option( 'wppb_default_form_ids', array() );
390 if ( ! is_array( $defaults ) || empty( $defaults ) ) return;
391
392 $old = is_array( $old_value ) ? $old_value : array();
393 $new = is_array( $new_value ) ? $new_value : array();
394
395 $gate_map = array(
396 'register' => 'pb-default-register',
397 'edit_profile' => 'pb-default-edit-profile',
398 );
399
400 foreach ( $gate_map as $key => $gate ) {
401 if ( empty( $defaults[ $key ] ) ) continue;
402
403 $was_on = ( isset( $old[ $gate ] ) ? $old[ $gate ] : 'no' ) === 'yes';
404 $now_on = ( isset( $new[ $gate ] ) ? $new[ $gate ] : 'no' ) === 'yes';
405 if ( ! $was_on || $now_on ) continue;
406
407 $post_id = (int) $defaults[ $key ];
408 delete_post_meta( $post_id, 'wppb_msf_break_points' );
409 delete_post_meta( $post_id, 'wppb_msf_tab_titles' );
410 update_post_meta( $post_id, 'wppb_msf_post_options', array(
411 'msf-pagination' => 'no',
412 'msf-tabs' => 'no',
413 ) );
414 }
415 }
416 function wppb_msf_fb_option_added( $option, $value ) {
417 if ( ! in_array( $option, array( 'wppb_msf_options', 'wppb_msf_break_points', 'wppb_msf_tab_titles' ), true ) ) return;
418 wppb_msf_fb_mirror_options_to_default_forms();
419 }
420
421