PluginProbe ʕ •ᴥ•ʔ
Secure Custom Fields / trunk
Secure Custom Fields vtrunk
6.9.0 6.8.9 6.8.7 6.8.8 6.8.6 6.8.4 6.8.5 trunk 6.4.0-beta1 6.4.0-beta2 6.4.1 6.4.1-beta3 6.4.1-beta4 6.4.1-beta5 6.4.1-beta6 6.4.1-beta7 6.4.2 6.5.0 6.5.1 6.5.2 6.5.3 6.5.4 6.5.5 6.5.6 6.5.7 6.6.0 6.7.0 6.7.1 6.8.0 6.8.1 6.8.2 6.8.3
secure-custom-fields / includes / Datastore / Localization.php
secure-custom-fields / includes / Datastore Last commit date
Check_Screen.php 1 month ago Localization.php 1 month ago REST_Save.php 1 month ago Revisions.php 1 month ago
Localization.php
466 lines
1 <?php
2 /**
3 * SCF datastore integration.
4 *
5 * @package wordpress/secure-custom-fields
6 */
7
8 namespace SCF\Datastore;
9
10 /**
11 * Enqueues the SCF datastore script and localizes field group definitions
12 * and values for the @wordpress/data store consumed by the JS datastore.
13 *
14 * Independently listens to the same enqueue_block_editor_assets WP action
15 * the free ACF_Form_Gutenberg uses, so no free-side touchpoint is required.
16 */
17 class Localization {
18
19 /**
20 * Constructor.
21 *
22 * @since ACF 6.8.1
23 */
24 public function __construct() {
25 add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue' ) );
26 add_filter( 'acf/ajax/query_users/args', array( $this, 'add_user_query_include' ), 20, 3 );
27 }
28
29 /**
30 * Allows the JS datastore to look up specific users by ID via the user
31 * query endpoint, so revision restores and programmatic acf.store.set()
32 * calls can render user labels for values not in the page-rendered options.
33 *
34 * @since ACF 6.8.1
35 *
36 * @param array $args The query args.
37 * @param array $request The query request.
38 * @param \ACF_Ajax_Query $query The query object.
39 * @return array
40 */
41 public function add_user_query_include( $args, $request, $query ) {
42 unset( $query );
43
44 if ( ! acf_is_using_datastore() ) {
45 return $args;
46 }
47 if ( empty( $request['include'] ) ) {
48 return $args;
49 }
50 $args['include'] = array( absint( $request['include'] ) );
51 return $args;
52 }
53
54 /**
55 * Enqueues the datastore script and localizes the field store data
56 * when the datastore is enabled.
57 *
58 * @since ACF 6.8.1
59 *
60 * @return void
61 */
62 public function enqueue() {
63 if ( ! acf_is_using_datastore() ) {
64 return;
65 }
66
67 wp_enqueue_script( 'acf-datastore' );
68 $this->localize_field_store_data();
69 }
70
71 /**
72 * Localizes field group definitions and values for the SCF @wordpress/data store.
73 *
74 * Gathers all field groups visible on the current post, serializes their
75 * field definitions and current values, and passes them to JS via
76 * acf_localize_data(). This data initializes the 'acf/fields' store
77 * which powers JS-side block bindings and developer access.
78 *
79 * @since ACF 6.8.1
80 *
81 * @return void
82 */
83 private function localize_field_store_data() {
84 global $post;
85
86 if ( ! $post ) {
87 return;
88 }
89
90 $field_groups = acf_get_field_groups( array( 'post_id' => $post->ID ) );
91
92 if ( empty( $field_groups ) ) {
93 return;
94 }
95
96 $store_data = array(
97 'context' => array(
98 'postId' => $post->ID,
99 'postType' => $post->post_type,
100 ),
101 'fields' => array(),
102 'values' => array(),
103 'fieldGroups' => array(),
104 );
105
106 foreach ( $field_groups as $field_group ) {
107 $store_data['fieldGroups'][] = array(
108 'key' => $field_group['key'],
109 'title' => acf_esc_html( acf_get_field_group_title( $field_group ) ),
110 'position' => $field_group['position'],
111 'style' => $field_group['style'],
112 'label_placement' => $field_group['label_placement'],
113 'instruction_placement' => $field_group['instruction_placement'],
114 'edit_url' => esc_url( acf_get_field_group_edit_link( $field_group['ID'] ) ),
115 );
116
117 $fields = acf_get_fields( $field_group );
118 if ( $fields ) {
119 $this->collect_field_data( $fields, $post->ID, $field_group['key'], $store_data );
120 }
121 }
122
123 acf_localize_data( array( 'storeData' => $store_data ) );
124 }
125
126 /**
127 * Recursively collects field definitions and values for the store.
128 *
129 * Processes an array of fields, loading each field's value and adding
130 * it to the store data structure. For complex fields (repeater, group,
131 * flexible content), recurses into sub-fields to build nested values.
132 *
133 * @since ACF 6.8.1
134 *
135 * @param array $fields Array of field arrays.
136 * @param integer $post_id The post ID to load values for.
137 * @param string $field_group_key The parent field group's key.
138 * @param array $store_data Reference to the store data being built.
139 * @return void
140 */
141 public function collect_field_data( $fields, $post_id, $field_group_key, &$store_data ) {
142 foreach ( $fields as $field ) {
143 $field = apply_filters( 'acf/prepare_field', $field );
144 if ( ! $field ) {
145 continue;
146 }
147
148 $field_def = array(
149 'key' => $field['key'],
150 'name' => $field['name'],
151 'type' => $field['type'],
152 'label' => acf_esc_html( $field['label'] ),
153 'parent' => $field['parent'],
154 'fieldGroupKey' => $field_group_key,
155 );
156
157 if ( ! acf_field_type_supports( $field['type'], 'bindings', true ) ) {
158 $field_def['supportsBindings'] = false;
159 }
160
161 if ( isset( $field['allow_in_bindings'] ) ) {
162 $field_def['allowInBindings'] = (bool) $field['allow_in_bindings'];
163 }
164
165 // Repeaters with pagination enabled need unique handling in the datastore.
166 if ( 'repeater' === $field['type'] ) {
167 $field_def['pagination'] = ! empty( $field['pagination'] );
168 }
169
170 // Include sub_fields metadata for complex types.
171 if ( ! empty( $field['sub_fields'] ) ) {
172 $field_def['subFields'] = array();
173 foreach ( $field['sub_fields'] as $sub_field ) {
174 if ( apply_filters( 'acf/prepare_field', $sub_field ) ) {
175 $field_def['subFields'][] = $sub_field['key'];
176 }
177 }
178 }
179
180 // Include layouts for flexible content.
181 if ( ! empty( $field['layouts'] ) ) {
182 $field_def['layouts'] = array();
183 foreach ( $field['layouts'] as $layout ) {
184 $layout_def = array(
185 'key' => $layout['key'],
186 'name' => $layout['name'],
187 'label' => acf_esc_html( $layout['label'] ),
188 );
189 if ( ! empty( $layout['sub_fields'] ) ) {
190 $layout_def['subFields'] = array();
191 foreach ( $layout['sub_fields'] as $sub_field ) {
192 if ( apply_filters( 'acf/prepare_field', $sub_field ) ) {
193 $layout_def['subFields'][] = $sub_field['key'];
194 }
195 }
196 }
197 $field_def['layouts'][] = $layout_def;
198 }
199 }
200
201 $store_data['fields'][ $field['key'] ] = $field_def;
202
203 // Load the field value.
204 //
205 // Skip paginated repeaters: the value would be discarded by
206 // reconcileWithDOM in JS (visible-page DOM rows overwrite it
207 // on init), and acf_get_value() would cache the unsliced row
208 // set here -- load_value()'s pagination slice is gated on
209 // $is_rendering, which isn't set until pre_render_fields fires
210 // for the metabox. The cached unsliced value would then be
211 // reused by the render and show all rows on page 1.
212 if ( 'repeater' === $field['type'] && ! empty( $field['pagination'] ) ) {
213 $store_data['values'][ $field['key'] ] = array();
214 } else {
215 $value = acf_get_value( $post_id, $field );
216 $store_data['values'][ $field['key'] ] = $this->serialize_field_value( $field, $value, $post_id );
217 }
218
219 // Register sub-fields in the store for complex types.
220 if ( ! empty( $field['sub_fields'] ) ) {
221 $this->register_sub_fields( $field['sub_fields'], $field_group_key, $store_data );
222 }
223
224 // Register layout sub-fields for flexible content.
225 if ( ! empty( $field['layouts'] ) ) {
226 foreach ( $field['layouts'] as $layout ) {
227 if ( ! empty( $layout['sub_fields'] ) ) {
228 $this->register_sub_fields( $layout['sub_fields'], $field_group_key, $store_data );
229 }
230 }
231 }
232 }
233 }
234
235 /**
236 * Registers sub-field definitions in the store (without loading values).
237 *
238 * Sub-field values are stored as part of their parent's value structure,
239 * but their definitions need to be in the store for metadata access.
240 *
241 * @since ACF 6.8.1
242 *
243 * @param array $sub_fields Array of sub-field arrays.
244 * @param string $field_group_key The parent field group's key.
245 * @param array $store_data Reference to the store data being built.
246 * @return void
247 */
248 public function register_sub_fields( $sub_fields, $field_group_key, &$store_data ) {
249 foreach ( $sub_fields as $sub_field ) {
250 $sub_field = apply_filters( 'acf/prepare_field', $sub_field );
251 if ( ! $sub_field ) {
252 continue;
253 }
254
255 // Bindings::get_value can't resolve sub-field keys at the
256 // post level (no row index for repeater/flex; no parent-chain
257 // prefix walk for group/clone), so the picker must exclude them.
258 $field_def = array(
259 'key' => $sub_field['key'],
260 'name' => $sub_field['name'],
261 'type' => $sub_field['type'],
262 'label' => acf_esc_html( $sub_field['label'] ),
263 'parent' => $sub_field['parent'],
264 'fieldGroupKey' => $field_group_key,
265 'supportsBindings' => false,
266 );
267
268 if ( ! empty( $sub_field['sub_fields'] ) ) {
269 $field_def['subFields'] = array();
270 foreach ( $sub_field['sub_fields'] as $nested ) {
271 if ( apply_filters( 'acf/prepare_field', $nested ) ) {
272 $field_def['subFields'][] = $nested['key'];
273 }
274 }
275 // Recurse for nested complex fields.
276 $this->register_sub_fields( $sub_field['sub_fields'], $field_group_key, $store_data );
277 }
278
279 if ( ! empty( $sub_field['layouts'] ) ) {
280 $field_def['layouts'] = array();
281 foreach ( $sub_field['layouts'] as $layout ) {
282 $layout_def = array(
283 'key' => $layout['key'],
284 'name' => $layout['name'],
285 'label' => acf_esc_html( $layout['label'] ),
286 );
287 if ( ! empty( $layout['sub_fields'] ) ) {
288 $layout_def['subFields'] = array();
289 foreach ( $layout['sub_fields'] as $nested ) {
290 if ( apply_filters( 'acf/prepare_field', $nested ) ) {
291 $layout_def['subFields'][] = $nested['key'];
292 }
293 }
294 $this->register_sub_fields( $layout['sub_fields'], $field_group_key, $store_data );
295 }
296 $field_def['layouts'][] = $layout_def;
297 }
298 }
299
300 $store_data['fields'][ $sub_field['key'] ] = $field_def;
301 }
302 }
303
304 /**
305 * Serializes a field value into the structure expected by the JS store.
306 *
307 * For simple fields, returns the raw value. For complex fields (repeater,
308 * group, flexible content), builds a nested structure matching the ACF
309 * REST API format.
310 *
311 * @since ACF 6.8.1
312 *
313 * @param array $field The field array.
314 * @param mixed $value The raw field value.
315 * @param integer $post_id The post ID.
316 * @return mixed The serialized value.
317 */
318 public function serialize_field_value( $field, $value, $post_id ) {
319 switch ( $field['type'] ) {
320 case 'repeater':
321 return $this->serialize_repeater_value( $field, $value, $post_id );
322
323 case 'group':
324 return $this->serialize_group_value( $field, $value, $post_id );
325
326 case 'flexible_content':
327 return $this->serialize_flexible_content_value( $field, $value, $post_id );
328
329 case 'wysiwyg':
330 return is_string( $value ) ? acf_esc_html( $value ) : $value;
331
332 default:
333 return $value;
334 }
335 }
336
337 /**
338 * Serializes a repeater field value.
339 *
340 * Acf_get_value() returns the loaded value from the repeater's load_value()
341 * method -- an array of rows keyed by sub-field key, not the raw row count
342 * stored in the database.
343 *
344 * @since ACF 6.8.1
345 *
346 * @param array $field The repeater field array.
347 * @param mixed $value The loaded value (array of rows from load_value).
348 * @param integer $post_id The post ID.
349 * @return array Array of row objects.
350 */
351 public function serialize_repeater_value( $field, $value, $post_id ) {
352 if ( empty( $field['sub_fields'] ) || ! is_array( $value ) ) {
353 return array();
354 }
355
356 $result = array();
357
358 foreach ( $value as $row ) {
359 if ( ! is_array( $row ) ) {
360 continue;
361 }
362
363 $serialized_row = array();
364
365 foreach ( $field['sub_fields'] as $sub_field ) {
366 $sub_value = isset( $row[ $sub_field['key'] ] ) ? $row[ $sub_field['key'] ] : null;
367 $serialized_row[ $sub_field['key'] ] = $this->serialize_field_value( $sub_field, $sub_value, $post_id );
368 }
369
370 $result[] = $serialized_row;
371 }
372
373 return $result;
374 }
375
376 /**
377 * Serializes a group field value.
378 *
379 * When called from a parent complex field (repeater, flex content), $value
380 * is the already-loaded array from load_value() keyed by sub-field key.
381 * For top-level groups, $value may also be a loaded array. Falls back to
382 * loading from the database when the loaded value is not available.
383 *
384 * @since ACF 6.8.1
385 *
386 * @param array $field The group field array.
387 * @param mixed $value The loaded value (array of sub-field values, or raw).
388 * @param integer $post_id The post ID.
389 * @return array|\stdClass Associative array of sub-field values keyed by field key.
390 */
391 public function serialize_group_value( $field, $value, $post_id ) {
392 if ( empty( $field['sub_fields'] ) ) {
393 return new \stdClass();
394 }
395
396 $result = array();
397
398 foreach ( $field['sub_fields'] as $sub_field ) {
399 if ( is_array( $value ) && array_key_exists( $sub_field['key'], $value ) ) {
400 $sub_value = $value[ $sub_field['key'] ];
401 } else {
402 $sub_field['name'] = $field['name'] . '_' . $sub_field['name'];
403 $sub_value = acf_get_value( $post_id, $sub_field );
404 }
405
406 $result[ $sub_field['key'] ] = $this->serialize_field_value( $sub_field, $sub_value, $post_id );
407 }
408
409 return $result;
410 }
411
412 /**
413 * Serializes a flexible content field value.
414 *
415 * Acf_get_value() returns the loaded value from the flex content's
416 * load_value() method -- an array of layout row objects, each containing
417 * an 'acf_fc_layout' key and sub-field values keyed by field key.
418 *
419 * @since ACF 6.8.1
420 *
421 * @param array $field The flexible content field array.
422 * @param mixed $value The loaded value (array of layout rows from load_value).
423 * @param integer $post_id The post ID.
424 * @return array Array of layout objects.
425 */
426 public function serialize_flexible_content_value( $field, $value, $post_id ) {
427 if ( ! is_array( $value ) || empty( $field['layouts'] ) ) {
428 return array();
429 }
430
431 // Build a lookup of layouts by name for sub-field definitions.
432 $layouts_by_name = array();
433 foreach ( $field['layouts'] as $layout ) {
434 $layouts_by_name[ $layout['name'] ] = $layout;
435 }
436
437 $result = array();
438
439 foreach ( $value as $row ) {
440 if ( ! is_array( $row ) || empty( $row['acf_fc_layout'] ) ) {
441 continue;
442 }
443
444 $layout_name = $row['acf_fc_layout'];
445
446 if ( ! isset( $layouts_by_name[ $layout_name ] ) ) {
447 continue;
448 }
449
450 $layout = $layouts_by_name[ $layout_name ];
451 $layout_row = array( 'acf_fc_layout' => $layout_name );
452
453 if ( ! empty( $layout['sub_fields'] ) ) {
454 foreach ( $layout['sub_fields'] as $sub_field ) {
455 $sub_value = isset( $row[ $sub_field['key'] ] ) ? $row[ $sub_field['key'] ] : null;
456 $layout_row[ $sub_field['key'] ] = $this->serialize_field_value( $sub_field, $sub_value, $post_id );
457 }
458 }
459
460 $result[] = $layout_row;
461 }
462
463 return $result;
464 }
465 }
466