PluginProbe
User Profile Builder – Beautiful User Registration Forms, User Profiles & User Role Editor / 4.0.0
User Profile Builder – Beautiful User Registration Forms, User Profiles & User Role Editor v4.0.0
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 / form-builder-rest-routes.php

form-builder-rest-routes.php in User Profile Builder – Beautiful User Registration Forms, User Profiles & User Role Editor 4.0.0, at form-builder/form-builder-rest-routes.php

411 lines 16.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Form-builder REST API. Admin-only (`manage_options`), standard `wp_rest` nonce.
4 */
5
6 if ( ! defined( 'ABSPATH' ) ) exit;
7
8 /** Admin-only permission callback for form-builder REST routes. */
9 function wppb_fb_rest_manage_options_permission() {
10 return current_user_can( 'manage_options' );
11 }
12
13 add_action( 'rest_api_init', 'wppb_fb_register_cf_rest_routes' );
14 function wppb_fb_register_cf_rest_routes() {
15 register_rest_route( 'wppb/v1', '/cf-source-options', array(
16 'methods' => 'GET',
17 'callback' => 'wppb_fb_rest_cf_source_options',
18 'permission_callback' => 'wppb_fb_rest_manage_options_permission',
19 'args' => array(
20 'field_type' => array( 'type' => 'string', 'required' => true ),
21 'cpt' => array( 'type' => 'string' ),
22 'taxonomy' => array( 'type' => 'string' ),
23 'user_roles' => array( 'type' => 'string' ),
24 ),
25 ) );
26
27 // Editor refetches after save for server-assigned ids and meta-names.
28 register_rest_route( 'wppb/v1', '/existing-fields', array(
29 'methods' => 'GET',
30 'callback' => function () {
31 return rest_ensure_response( array( 'fields' => wppb_fb_existing_fields_bridge() ) );
32 },
33 'permission_callback' => 'wppb_fb_rest_manage_options_permission',
34 ) );
35
36 register_rest_route( 'wppb/v1', '/existing-fields/(?P<id>\d+)', array(
37 array(
38 'methods' => 'PUT',
39 'callback' => 'wppb_fb_rest_update_existing_field',
40 'permission_callback' => 'wppb_fb_rest_manage_options_permission',
41 'args' => array(
42 'id' => array( 'type' => 'integer', 'required' => true ),
43 'attributes' => array( 'type' => 'object', 'required' => true ),
44 'field' => array( 'type' => 'string' ),
45 ),
46 ),
47 array(
48 'methods' => 'DELETE',
49 'callback' => 'wppb_fb_rest_delete_existing_field',
50 'permission_callback' => 'wppb_fb_rest_manage_options_permission',
51 'args' => array(
52 'id' => array( 'type' => 'integer', 'required' => true ),
53 ),
54 ),
55 ) );
56
57 // Must register before {id}; `$` anchor stops /{id}/usage matching that route.
58 register_rest_route( 'wppb/v1', '/existing-fields/(?P<id>\d+)/usage', array(
59 'methods' => 'GET',
60 'callback' => 'wppb_fb_rest_existing_field_usage',
61 'permission_callback' => 'wppb_fb_rest_manage_options_permission',
62 'args' => array(
63 'id' => array( 'type' => 'integer', 'required' => true ),
64 ),
65 ) );
66
67 // Sub-fields live in per-Repeater options, not wppb_manage_fields.
68 register_rest_route( 'wppb/v1', '/existing-fields/sub/(?P<parent_meta>[A-Za-z0-9_\-]+)/(?P<sub_id>\d+)', array(
69 array(
70 'methods' => 'PUT',
71 'callback' => 'wppb_fb_rest_update_existing_subfield',
72 'permission_callback' => 'wppb_fb_rest_manage_options_permission',
73 'args' => array(
74 'parent_meta' => array( 'type' => 'string', 'required' => true ),
75 'sub_id' => array( 'type' => 'integer', 'required' => true ),
76 'attributes' => array( 'type' => 'object', 'required' => true ),
77 'field' => array( 'type' => 'string' ),
78 ),
79 ),
80 ) );
81
82 // Next free id from global pool plus client `claimed`; save-time dedup handles races.
83 register_rest_route( 'wppb/v1', '/allocate-field-id', array(
84 'methods' => 'POST',
85 'callback' => 'wppb_fb_rest_allocate_field_id',
86 'permission_callback' => 'wppb_fb_rest_manage_options_permission',
87 'args' => array(
88 'claimed' => array(
89 'type' => 'array',
90 'items' => array( 'type' => 'integer' ),
91 'default' => array(),
92 ),
93 ),
94 ) );
95 }
96
97 function wppb_fb_rest_allocate_field_id( WP_REST_Request $request ) {
98 $manage = wppb_fb_manage_fields();
99 if ( ! is_array( $manage ) ) $manage = array();
100
101 $extra_ids = wppb_fb_collect_global_subfield_ids( $manage );
102 $claimed = (array) $request->get_param( 'claimed' );
103 $claimed = array_values( array_filter( array_map( 'intval', $claimed ), function ( $i ) { return $i > 0; } ) );
104
105 $id = wppb_fb_next_field_id( $manage, array_merge( $extra_ids, $claimed ) );
106 return rest_ensure_response( array( 'id' => (int) $id ) );
107 }
108
109 /**
110 * Shared upsert for top-level and sub-field PUT routes.
111 *
112 * @param array $collection Row collection (manage_fields or Repeater group).
113 * @param int $id Row id to find or create.
114 * @param array $attrs Attribute patch.
115 * @param string $field_hint Field type when creating (404 if missing).
116 * @param string $not_found_code WP_Error code when row absent and no hint.
117 * @param callable $build_context Context for wppb_fb_apply_field_row_patch().
118 * @return array|WP_Error collection, row, created; or WP_Error.
119 */
120 function wppb_fb_rest_upsert_row_core( array $collection, $id, array $attrs, $field_hint, $not_found_code, callable $build_context ) {
121 $id = (int) $id;
122
123 $row_index = null;
124 foreach ( $collection as $idx => $row ) {
125 if ( isset( $row['id'] ) && (int) $row['id'] === $id ) {
126 $row_index = $idx;
127 break;
128 }
129 }
130
131 $created = false;
132 if ( $row_index === null ) {
133 if ( $field_hint === '' ) {
134 return new WP_Error(
135 $not_found_code,
136 'Row not found. Include `field` in the request body to create it.',
137 array( 'status' => 404 )
138 );
139 }
140 $registry = WPPB_FB_Field_Registry::all();
141 if ( ! isset( $registry[ $field_hint ] ) ) {
142 return new WP_Error( 'wppb_unknown_field_type', 'Unknown field type: ' . $field_hint, array( 'status' => 400 ) );
143 }
144 $collection[] = array( 'id' => $id, 'field' => $field_hint );
145 $row_index = count( $collection ) - 1;
146 $created = true;
147 }
148
149 $row = $collection[ $row_index ];
150 $field_type = isset( $row['field'] ) ? (string) $row['field'] : '';
151
152 $claimed = array();
153 foreach ( $collection as $idx2 => $other ) {
154 if ( $idx2 === $row_index ) continue;
155 if ( isset( $other['meta-name'] ) && $other['meta-name'] !== '' ) {
156 $claimed[ $other['meta-name'] ] = true;
157 }
158 }
159
160 $context = call_user_func( $build_context, $field_type, $collection, $claimed, $row );
161 $row = wppb_fb_apply_field_row_patch( $row, $attrs, $context );
162
163 $collection[ $row_index ] = $row;
164 return array( 'collection' => $collection, 'row' => $row, 'created' => $created );
165 }
166
167 /**
168 * Upsert wppb_manage_fields row. Mirror may create on first edit; `field` required when absent.
169 * Blocks Repeater meta-name changes (re-key orphans sub-field option and user data).
170 */
171 function wppb_fb_rest_update_existing_field( WP_REST_Request $request ) {
172 $id = (int) $request['id'];
173 $attrs = $request->get_param( 'attributes' );
174 if ( ! is_array( $attrs ) ) {
175 return new WP_Error( 'wppb_invalid_attributes', 'attributes must be an object', array( 'status' => 400 ) );
176 }
177
178 $manage = wppb_fb_manage_fields();
179 if ( ! is_array( $manage ) ) $manage = array();
180
181 $result = wppb_fb_rest_upsert_row_core(
182 $manage,
183 $id,
184 $attrs,
185 (string) $request->get_param( 'field' ),
186 'wppb_field_not_found',
187 function ( $field_type, $collection, $claimed, $row ) use ( $attrs ) {
188 $skip_repeater_edit = ( $field_type === 'Repeater' )
189 && isset( $row['meta-name'] ) && $row['meta-name'] !== ''
190 && array_key_exists( 'meta-name', $attrs );
191 // New Repeater needs field-title or auto-slug falls back to wppb_repeater_field_group.
192 $field_title = isset( $attrs['field-title'] )
193 ? (string) $attrs['field-title']
194 : ( isset( $row['field-title'] ) ? (string) $row['field-title'] : '' );
195 return array(
196 'field_type' => $field_type,
197 'manage' => $collection,
198 'claimed' => $claimed,
199 'field_title' => $field_title,
200 'resolve_meta_name' => ! $skip_repeater_edit,
201 );
202 }
203 );
204 if ( is_wp_error( $result ) ) return $result;
205
206 update_option( 'wppb_manage_fields', $result['collection'] );
207
208 $registry = WPPB_FB_Field_Registry::all();
209 $entry = wppb_fb_existing_fields_row_to_entry( $result['row'], $registry );
210 if ( $entry === null ) {
211 return new WP_Error( 'wppb_field_unrepresentable', 'Field could not be re-projected', array( 'status' => 500 ) );
212 }
213 wppb_fb_hydrate_repeater_subfields( $entry, $registry );
214
215 $response = rest_ensure_response( array( 'field' => $entry, 'created' => $result['created'] ) );
216 if ( $result['created'] ) $response->set_status( 201 );
217 return $response;
218 }
219
220 /**
221 * Upsert one Repeater sub-field in get_option( $parent_meta ).
222 * Meta-name uniqueness is per-Repeater; global manage_fields used for reverse collision check.
223 */
224 function wppb_fb_rest_update_existing_subfield( WP_REST_Request $request ) {
225 $parent_meta = (string) $request['parent_meta'];
226 $sub_id = (int) $request['sub_id'];
227 $attrs = $request->get_param( 'attributes' );
228 if ( ! is_array( $attrs ) ) {
229 return new WP_Error( 'wppb_invalid_attributes', 'attributes must be an object', array( 'status' => 400 ) );
230 }
231 if ( $parent_meta === '' ) {
232 return new WP_Error( 'wppb_invalid_parent', 'parent_meta is required', array( 'status' => 400 ) );
233 }
234
235 // parent_meta must be a real Repeater meta-name; else get_option/update_option could clobber core options.
236 $global_manage = wppb_fb_manage_fields();
237 if ( ! is_array( $global_manage ) ) $global_manage = array();
238 if ( ! wppb_fb_is_repeater_meta_name( $parent_meta, $global_manage ) ) {
239 return new WP_Error(
240 'wppb_invalid_parent',
241 'parent_meta is not a known Repeater meta-name',
242 array( 'status' => 400 )
243 );
244 }
245
246 $group = get_option( $parent_meta, 'not_set' );
247 if ( $group === 'not_set' || ! is_array( $group ) ) $group = array();
248
249 $result = wppb_fb_rest_upsert_row_core(
250 $group,
251 $sub_id,
252 $attrs,
253 (string) $request->get_param( 'field' ),
254 'wppb_subfield_not_found',
255 function ( $field_type, $collection, $claimed, $row ) use ( $global_manage ) {
256 return array(
257 'field_type' => $field_type,
258 'manage' => $global_manage,
259 'claimed' => $claimed,
260 'is_subfield' => true,
261 'group' => $collection,
262 );
263 }
264 );
265 if ( is_wp_error( $result ) ) return $result;
266
267 update_option( $parent_meta, $result['collection'] );
268
269 $registry = WPPB_FB_Field_Registry::all();
270 $entry = wppb_fb_existing_fields_row_to_entry( $result['row'], $registry, true );
271 if ( $entry === null ) {
272 return new WP_Error( 'wppb_subfield_unrepresentable', 'Sub-field could not be re-projected', array( 'status' => 500 ) );
273 }
274
275 $response = rest_ensure_response( array( 'field' => $entry, 'created' => $result['created'], 'parent_meta' => $parent_meta ) );
276 if ( $result['created'] ) $response->set_status( 201 );
277 return $response;
278 }
279
280 /**
281 * Delete one manage_fields row. Fires wck_before_remove_meta before update (listeners re-read option).
282 * Blocks mandatory registration fields (username, email, password).
283 */
284 function wppb_fb_rest_delete_existing_field( WP_REST_Request $request ) {
285 $id = (int) $request['id'];
286
287 $manage = wppb_fb_manage_fields();
288 if ( ! is_array( $manage ) ) $manage = array();
289
290 $row_index = null;
291 foreach ( $manage as $idx => $row ) {
292 if ( isset( $row['id'] ) && (int) $row['id'] === $id ) {
293 $row_index = $idx;
294 break;
295 }
296 }
297 if ( $row_index === null ) {
298 return new WP_Error( 'wppb_field_not_found', 'Field not found', array( 'status' => 404 ) );
299 }
300
301 $row = $manage[ $row_index ];
302 $field_type = isset( $row['field'] ) ? (string) $row['field'] : '';
303
304 if ( in_array( $field_type, wppb_fb_mandatory_registration_field_types(), true ) ) {
305 return new WP_Error(
306 'wppb_field_undeletable',
307 __( 'Username, E-mail and Password fields cannot be deleted.', 'profile-builder' ),
308 array( 'status' => 403 )
309 );
310 }
311
312 // Must run before update_option; ob_start swallows echoed JS from WCK listeners.
313 ob_start();
314 do_action( 'wck_before_remove_meta', 'wppb_manage_fields', 0, $row_index );
315 ob_end_clean();
316
317 unset( $manage[ $row_index ] );
318 $manage = array_values( $manage );
319 update_option( 'wppb_manage_fields', $manage );
320
321 return rest_ensure_response( array( 'id' => $id, 'deleted' => true ) );
322 }
323
324 function wppb_fb_rest_existing_field_usage( WP_REST_Request $request ) {
325 $id = (int) $request['id'];
326 return rest_ensure_response( array(
327 'id' => $id,
328 'forms' => wppb_fb_forms_using_field( $id ),
329 ) );
330 }
331
332 /**
333 * Value/label pairs for enriched Select types where stored value differs from displayed label.
334 */
335 function wppb_fb_rest_cf_source_options( WP_REST_Request $request ) {
336 $field_type = (string) $request->get_param( 'field_type' );
337 $values = array();
338 $labels = array();
339
340 if ( $field_type === 'Select (CPT)' ) {
341 $cpt = (string) $request->get_param( 'cpt' );
342 if ( $cpt !== '' && post_type_exists( $cpt ) ) { // same guard as taxonomy branch
343 $query = new WP_Query( array(
344 'post_type' => $cpt,
345 'orderby' => 'menu_order title',
346 'order' => 'ASC',
347 'posts_per_page' => 200,
348 'post_status' => 'publish',
349 ) );
350 foreach ( $query->posts as $cpt_post ) {
351 $values[] = (string) $cpt_post->ID;
352 $labels[] = $cpt_post->post_title !== '' ? $cpt_post->post_title : 'No title. ID: ' . $cpt_post->ID;
353 }
354 }
355 } elseif ( $field_type === 'Select (Taxonomy)' ) {
356 $taxonomy = (string) $request->get_param( 'taxonomy' );
357 if ( $taxonomy !== '' && taxonomy_exists( $taxonomy ) ) {
358 $args = apply_filters( 'wppb_taxonomy_select_args', array(
359 'taxonomy' => $taxonomy,
360 'hide_empty' => false,
361 'number' => 200, // match CPT cap
362 ), array( 'taxonomy' => $taxonomy ) );
363 $terms = get_terms( $args );
364 if ( ! is_wp_error( $terms ) ) {
365 foreach ( $terms as $term ) {
366 $values[] = (string) $term->term_id;
367 $labels[] = $term->name;
368 }
369 }
370 }
371 } elseif ( $field_type === 'Select (User Role)' ) {
372 $user_roles_csv = (string) $request->get_param( 'user_roles' );
373 if ( $user_roles_csv !== '' ) {
374 global $wp_roles;
375 foreach ( explode( ',', $user_roles_csv ) as $slug ) {
376 $slug = trim( $slug );
377 if ( $slug === '' ) continue;
378 $values[] = $slug;
379 $labels[] = isset( $wp_roles->roles[ $slug ]['name'] ) ? $wp_roles->roles[ $slug ]['name'] : $slug;
380 }
381 }
382 } elseif ( $field_type === 'Select (Country)' && function_exists( 'wppb_country_select_options' ) ) {
383 // Value is ISO code; label is country name (CL rules compare against ISO).
384 foreach ( wppb_country_select_options( 'register' ) as $iso => $country_name ) {
385 if ( $iso === '' ) continue; // skip the "Select a Country" placeholder
386 $values[] = (string) $iso;
387 $labels[] = $country_name;
388 }
389 } elseif ( $field_type === 'Select (Currency)' && function_exists( 'wppb_get_currencies' ) ) {
390 // Same value-vs-label split as country.
391 foreach ( wppb_get_currencies() as $iso => $currency_name ) {
392 if ( $iso === '' ) continue;
393 $label = $currency_name;
394 if ( function_exists( 'wppb_get_currency_symbol' ) ) {
395 $symbol = wppb_get_currency_symbol( $iso );
396 if ( ! empty( $symbol ) ) {
397 // Stored as HTML entities; decode so the control shows a glyph.
398 $label = $currency_name . ' (' . html_entity_decode( $symbol, ENT_QUOTES, 'UTF-8' ) . ')';
399 }
400 }
401 $values[] = (string) $iso;
402 $labels[] = $label;
403 }
404 }
405
406 return rest_ensure_response( array(
407 'values' => $values,
408 'labels' => $labels,
409 ) );
410 }
411