PluginProbe ʕ •ᴥ•ʔ
Secure Custom Fields / 6.8.8
Secure Custom Fields v6.8.8
6.9.2 6.9.1 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 / fields / class-acf-field-flexible-content.php
secure-custom-fields / includes / fields Last commit date
FlexibleContent 3 months ago class-acf-field-accordion.php 2 months ago class-acf-field-button-group.php 2 months ago class-acf-field-checkbox.php 2 months ago class-acf-field-clone.php 3 months ago class-acf-field-color_picker.php 2 months ago class-acf-field-date_picker.php 2 months ago class-acf-field-date_time_picker.php 2 months ago class-acf-field-email.php 2 months ago class-acf-field-file.php 2 months ago class-acf-field-flexible-content.php 1 month ago class-acf-field-gallery.php 1 month ago class-acf-field-google-map.php 2 months ago class-acf-field-group.php 2 months ago class-acf-field-icon_picker.php 8 months ago class-acf-field-image.php 2 months ago class-acf-field-link.php 2 months ago class-acf-field-message.php 1 year ago class-acf-field-nav-menu.php 1 year ago class-acf-field-number.php 2 months ago class-acf-field-oembed.php 1 month ago class-acf-field-output.php 1 year ago class-acf-field-page_link.php 1 month ago class-acf-field-password.php 2 months ago class-acf-field-post_object.php 1 month ago class-acf-field-radio.php 2 months ago class-acf-field-range.php 2 months ago class-acf-field-relationship.php 1 month ago class-acf-field-repeater.php 1 month ago class-acf-field-select.php 1 month ago class-acf-field-separator.php 1 year ago class-acf-field-tab.php 1 year ago class-acf-field-taxonomy.php 1 month ago class-acf-field-text.php 2 months ago class-acf-field-textarea.php 2 months ago class-acf-field-time_picker.php 2 months ago class-acf-field-true_false.php 2 months ago class-acf-field-url.php 2 months ago class-acf-field-user.php 1 month ago class-acf-field-wysiwyg.php 2 months ago class-acf-field.php 2 months ago class-acf-repeater-table.php 1 year ago index.php 1 year ago
class-acf-field-flexible-content.php
1714 lines
1 <?php
2 /**
3 * The Flexible Content Field.
4 *
5 * @package wordpress/secure-custom-fields
6 */
7
8 use SCF\Fields\FlexibleContent\Render;
9 use SCF\Fields\FlexibleContent\Layout;
10 // phpcs:disable PEAR.NamingConventions.ValidClassName
11 if ( ! class_exists( 'acf_field_flexible_content' ) ) :
12
13 /**
14 * The Flexible Content Field class.
15 */
16 class acf_field_flexible_content extends acf_field {
17
18 /**
19 * The post/page ID that we're rendering for.
20 *
21 * @var mixed
22 */
23 public $post_id = false;
24
25 /**
26 * An array of layout meta for the current field.
27 *
28 * @var array
29 */
30 public $layout_meta = array();
31
32 /**
33 * This function will setup the field type data
34 *
35 * @type function
36 * @date 5/03/2014
37 * @since ACF 5.0.0
38 */
39 public function initialize() {
40
41 // vars
42 $this->name = 'flexible_content';
43 $this->label = __( 'Flexible Content', 'secure-custom-fields' );
44 $this->category = 'layout';
45 $this->description = __( 'Allows you to define, create and manage content with total control by creating layouts that contain subfields that content editors can choose from.', 'secure-custom-fields' ) . ' ' . __( 'We do not recommend using this field in ACF Blocks.', 'secure-custom-fields' );
46 $this->preview_image = acf_get_url() . '/assets/images/field-type-previews/field-preview-flexible-content.png';
47 $this->doc_url = 'https://developer.wordpress.org/secure-custom-fields/features/fields/flexible-content/';
48 $this->tutorial_url = 'https://developer.wordpress.org/secure-custom-fields/features/fields/flexible-content/flexible-content-tutorial/';
49 $this->pro = true;
50 $this->supports = array( 'bindings' => false );
51 $this->defaults = array(
52 'layouts' => array(),
53 'min' => '',
54 'max' => '',
55 'button_label' => __( 'Add Row', 'secure-custom-fields' ),
56 );
57
58 // ajax
59 $this->add_action( 'wp_ajax_acf/fields/flexible_content/layout_title', array( $this, 'ajax_layout_title' ) );
60 $this->add_action( 'wp_ajax_nopriv_acf/fields/flexible_content/layout_title', array( $this, 'ajax_layout_title' ) );
61
62 // filters
63 $this->add_filter( 'acf/prepare_field_for_export', array( $this, 'prepare_any_field_for_export' ) );
64 $this->add_filter( 'acf/clone_field', array( $this, 'clone_any_field' ), 10, 2 );
65 $this->add_filter( 'acf/validate_field', array( $this, 'validate_any_field' ) );
66 $this->add_filter( 'acf/pre_render_fields', array( $this, 'pre_render_fields' ), 10, 2 );
67
68 // field filters
69 $this->add_field_filter( 'acf/get_sub_field', array( $this, 'get_sub_field' ), 10, 3 );
70 $this->add_field_filter( 'acf/prepare_field_for_export', array( $this, 'prepare_field_for_export' ) );
71 $this->add_field_filter( 'acf/prepare_field_for_import', array( $this, 'prepare_field_for_import' ) );
72 }
73
74
75 /**
76 * Admin scripts enqueue for field.
77 *
78 * @date 16/12/2015
79 * @since ACF 5.3.2
80 *
81 * @return void
82 */
83 public function input_admin_enqueue_scripts() {
84
85 // localize
86 acf_localize_text(
87 array(
88
89 // identifiers
90 'layout' => esc_html__( 'layout', 'secure-custom-fields' ),
91 'layouts' => esc_html__( 'layouts', 'secure-custom-fields' ),
92 'Fields' => esc_html__( 'Fields', 'secure-custom-fields' ),
93
94 // Adding/deleting a layout.
95 'Duplicate' => esc_html__( 'Duplicate', 'secure-custom-fields' ),
96 'Delete' => esc_html__( 'Delete', 'secure-custom-fields' ),
97 'Delete Layout' => esc_html__( 'Delete Layout', 'secure-custom-fields' ),
98 /* translators: %s - Name of the Flexible content layout */
99 'Delete %s' => esc_html__( 'Delete %s', 'secure-custom-fields' ),
100 'Are you sure you want to delete the layout?' => esc_html__( 'Are you sure you want to delete the layout?', 'secure-custom-fields' ),
101 /* translators: %s - Name of the Flexible content layout */
102 'Are you sure you want to delete %s?' => esc_html__( 'Are you sure you want to delete %s?', 'secure-custom-fields' ),
103
104 // Renaming a layout.
105 'Rename Layout' => esc_html__( 'Rename Layout', 'secure-custom-fields' ),
106 'Rename' => esc_html__( 'Rename', 'secure-custom-fields' ),
107 'New Label' => esc_html__( 'New Label', 'secure-custom-fields' ),
108 'Remove Custom Label' => esc_html__( 'Remove Custom Label', 'secure-custom-fields' ),
109
110 // min / max
111 'This field requires at least {min} {label} {identifier}' => __( 'This field requires at least {min} {label} {identifier}', 'secure-custom-fields' ),
112 'This field has a limit of {max} {label} {identifier}' => __( 'This field has a limit of {max} {label} {identifier}', 'secure-custom-fields' ),
113 'Maximum rows reached ({max})' => esc_html__( 'Maximum rows reached ({max})', 'secure-custom-fields' ),
114 'Maximum {label} {identifier} reached ({max})' => esc_html__( 'Maximum {label} {identifier} reached ({max})', 'secure-custom-fields' ),
115
116 // popup badge
117 '{available} {label} {identifier} available (max {max})' => __( '{available} {label} {identifier} available (max {max})', 'secure-custom-fields' ),
118 '{required} {label} {identifier} required (min {min})' => __( '{required} {label} {identifier} required (min {min})', 'secure-custom-fields' ),
119
120 // field settings
121 'Flexible Content requires at least 1 layout' => __( 'Flexible Content requires at least 1 layout', 'secure-custom-fields' ),
122 )
123 );
124 }
125
126
127 /**
128 * This function will fill in the missing keys to create a valid layout
129 *
130 * @type function
131 * @date 3/10/13
132 * @since ACF 1.1.0
133 *
134 * @param array $layout The layout array to validate.
135 * @return array $layout The validated layout array.
136 */
137 public function get_valid_layout( $layout = array() ) {
138
139 // parse
140 $layout = wp_parse_args(
141 $layout,
142 array(
143 'key' => uniqid( 'layout_' ),
144 'name' => '',
145 'label' => '',
146 'display' => 'block',
147 'sub_fields' => array(),
148 'min' => '',
149 'max' => '',
150 )
151 );
152
153 // return
154 return $layout;
155 }
156
157
158 /**
159 * Load a field.
160 *
161 * @since ACF 3.6
162 *
163 * @param array $field the field array holding all the field options.
164 *
165 * @return array $field the field array holding all the field options.
166 */
167 public function load_field( $field ) {
168
169 // bail early if no field layouts
170 if ( empty( $field['layouts'] ) ) {
171 return $field;
172 }
173
174 // vars
175 $sub_fields = acf_get_fields( $field );
176
177 // loop through layouts, sub fields and swap out the field key with the real field
178 foreach ( array_keys( $field['layouts'] ) as $i ) {
179
180 // extract layout
181 $layout = acf_extract_var( $field['layouts'], $i );
182
183 // validate layout
184 $layout = $this->get_valid_layout( $layout );
185
186 // append sub fields
187 if ( ! empty( $sub_fields ) ) {
188 foreach ( array_keys( $sub_fields ) as $k ) {
189
190 // check if 'parent_layout' is empty
191 if ( empty( $sub_fields[ $k ]['parent_layout'] ) ) {
192
193 // parent_layout did not save for this field, default it to first layout
194 $sub_fields[ $k ]['parent_layout'] = $layout['key'];
195 }
196
197 // append sub field to layout,
198 if ( $sub_fields[ $k ]['parent_layout'] == $layout['key'] ) { // phpcs:ignore Universal.Operators.StrictComparisons -- @todo Confirm types used here.
199 $layout['sub_fields'][] = acf_extract_var( $sub_fields, $k );
200 }
201 }
202 }
203
204 // append back to layouts
205 $field['layouts'][ $i ] = $layout;
206 }
207
208 // return
209 return $field;
210 }
211
212
213 /**
214 * This function will return a specific sub field.
215 *
216 * @type function
217 * @date 29/09/2016
218 * @since ACF 5.4.0
219 *
220 * @param int $sub_field Sub field.
221 * @param int|string $id the ID.
222 * @param array $field Field array.
223 * @return int
224 */
225 public function get_sub_field( $sub_field, $id, $field ) {
226
227 // Get active layout.
228 $active = get_row_layout();
229
230 // Loop over layouts.
231 if ( $field['layouts'] ) {
232 foreach ( $field['layouts'] as $layout ) {
233
234 // Restrict to active layout if within a have_rows() loop.
235 if ( $active && $active !== $layout['name'] ) {
236 continue;
237 }
238
239 // Check sub fields.
240 if ( $layout['sub_fields'] ) {
241 $sub_field = acf_search_fields( $id, $layout['sub_fields'] );
242 if ( $sub_field ) {
243 break;
244 }
245 }
246 }
247 }
248
249 // return
250 return $sub_field;
251 }
252
253 /**
254 * Runs on the "acf/pre_render_fields" filter. Used to signify
255 * that we're currently rendering a Flexible Content field.
256 *
257 * @since ACF 6.5
258 *
259 * @param array $fields The main field array.
260 * @param mixed $post_id The post ID for the field being rendered.
261 * @return array
262 */
263 public function pre_render_fields( $fields, $post_id = false ) {
264 if ( is_admin() ) {
265 $this->post_id = $post_id;
266 }
267
268 return $fields;
269 }
270
271 /**
272 * Create the HTML interface for your field
273 *
274 * @since ACF 3.6
275 *
276 * @param array $field An array holding all the field's data.
277 * @return void
278 */
279 public function render_field( $field ) {
280
281 // Add some defaults.
282 if ( empty( $field['button_label'] ) ) {
283 $field['button_label'] = $this->defaults['button_label'];
284 }
285
286 // Render the field.
287 $renderer = new Render(
288 $field,
289 $this->get_layout_meta( $this->post_id, $field )
290 );
291
292 $renderer->render();
293 }
294
295 /**
296 * Description
297 *
298 * @type function
299 * @date 19/11/2013
300 * @since ACF 5.0.0
301 *
302 * @param array $field Fields.
303 * @param array $layout Layout.
304 * @param integer|string $i row number.
305 * @param mixed $value Value.
306 * @return void
307 */
308 public function render_layout( $field, $layout, $i, $value ) {
309 $disabled_layouts = $this->get_disabled_layouts( $this->post_id, $field );
310 $renamed_layouts = $this->get_renamed_layouts( $this->post_id, $field );
311 $layout_disabled = in_array( $i, $disabled_layouts, true );
312 $renamed = ! empty( $renamed_layouts[ $i ] ) ? $renamed_layouts[ $i ] : '';
313
314 $layout = new Layout( $field, $layout, $i, $value, $layout_disabled, $renamed );
315 $layout->render();
316 }
317
318 /**
319 * Renders the flexible content field layouts in the field group editor.
320 *
321 * @since ACF 3.6
322 * @date 23/01/13
323 *
324 * @param array $field An array holding all the field's data.
325 */
326 public function render_field_settings( $field ) {
327 $layout_open = apply_filters( 'acf/fields/flexible_content/layout_default_expanded', false );
328
329 // Load default layout.
330 if ( empty( $field['layouts'] ) ) {
331 $layout_open = true;
332 $field['layouts'] = array(
333 array(),
334 );
335 }
336
337 $field_settings_class = $layout_open ? 'open' : '';
338 $toggle_class = $layout_open ? 'open' : 'closed';
339 $field_settings_style = $layout_open ? '' : 'display: none;';
340
341 // loop through layouts
342 foreach ( $field['layouts'] as $layout ) {
343
344 // get valid layout
345 $layout = $this->get_valid_layout( $layout );
346
347 // vars
348 $layout_prefix = "{$field['prefix']}[layouts][{$layout['key']}]";
349
350 ?>
351 <div class="acf-field acf-field-setting-fc_layout" data-name="fc_layout" data-setting="flexible_content" data-layout-label="<?php echo esc_attr( $layout['label'] ); ?>" data-layout-name="<?php echo esc_attr( $layout['name'] ); ?>" data-id="<?php echo esc_attr( $layout['key'] ); ?>">
352 <div class="acf-label acf-field-settings-fc_head">
353 <div class="acf-fc_draggable">
354 <label class="acf-fc-layout-label reorder-layout"><?php esc_attr_e( 'Layout', 'secure-custom-fields' ); ?></label>
355 </div>
356
357 <div class="acf-fc-layout-name copyable">
358 <span class="layout-name"></span>
359 </div>
360
361 <ul class="acf-bl acf-fl-actions">
362 <li><button class="acf-btn acf-btn-tertiary acf-btn-sm acf-field-setting-fc-delete"><i class="acf-icon acf-icon-trash delete-layout " href="#" title="<?php esc_attr_e( 'Delete Layout', 'secure-custom-fields' ); ?>"></i></button></li>
363 <li><button class="acf-btn acf-btn-tertiary acf-btn-sm acf-field-setting-fc-duplicate"><i class="acf-icon -duplicate duplicate-layout" href="#" title="<?php esc_attr_e( 'Duplicate Layout', 'secure-custom-fields' ); ?>"></i></button></li>
364 <li class="acf-fc-add-layout"><button class="add-layout acf-btn acf-btn-primary add-field" href="#" title="<?php esc_attr_e( 'Add New Layout', 'secure-custom-fields' ); ?>"><i class="acf-icon acf-icon-plus"></i><?php esc_html_e( 'Add Layout', 'secure-custom-fields' ); ?></button></li>
365 <li><button type="button" class="acf-toggle-fc-layout" aria-expanded="true"></li>
366 <li><span class="toggle-indicator <?php echo esc_attr( $toggle_class ); ?>" aria-hidden="true"></span></li>
367 </ul>
368 </div>
369 <div class="acf-input acf-field-layout-settings <?php echo esc_attr( $field_settings_class ); ?>" style="<?php echo esc_attr( $field_settings_style ); ?>">
370 <?php
371
372 acf_hidden_input(
373 array(
374 'id' => acf_idify( $layout_prefix . '[key]' ),
375 'name' => $layout_prefix . '[key]',
376 'class' => 'layout-key',
377 'value' => $layout['key'],
378 )
379 );
380
381 ?>
382 <ul class="acf-fc-meta acf-bl">
383 <li class="acf-fc-meta-label acf-fc-meta-left">
384 <?php
385
386 acf_render_field(
387 array(
388 'type' => 'text',
389 'name' => 'label',
390 'class' => 'layout-label',
391 'prefix' => $layout_prefix,
392 'value' => $layout['label'],
393 'prepend' => __( 'Label', 'secure-custom-fields' ),
394 )
395 );
396
397 ?>
398 </li>
399 <li class="acf-fc-meta-name acf-fc-meta-right copyable input-copyable">
400 <?php
401
402 acf_render_field(
403 array(
404 'type' => 'text',
405 'name' => 'name',
406 'class' => 'layout-name',
407 'input-data' => array( '1p-ignore' => 'true' ),
408 'prefix' => $layout_prefix,
409 'value' => $layout['name'],
410 'prepend' => __( 'Name', 'secure-custom-fields' ),
411 )
412 );
413
414 ?>
415 </li>
416 <li class="acf-fc-meta-display acf-fc-meta-left">
417 <div class="acf-input-prepend"><?php esc_html_e( 'Layout', 'secure-custom-fields' ); ?></div>
418 <div class="acf-input-wrap">
419 <?php
420
421 acf_render_field(
422 array(
423 'type' => 'select',
424 'name' => 'display',
425 'prefix' => $layout_prefix,
426 'value' => $layout['display'],
427 'class' => 'acf-is-prepended',
428 'choices' => array(
429 'table' => __( 'Table', 'secure-custom-fields' ),
430 'block' => __( 'Block', 'secure-custom-fields' ),
431 'row' => __( 'Row', 'secure-custom-fields' ),
432 ),
433 )
434 );
435
436 ?>
437 </div>
438 </li>
439 <li class="acf-fc-meta-min">
440 <?php
441
442 acf_render_field(
443 array(
444 'type' => 'text',
445 'name' => 'min',
446 'prefix' => $layout_prefix,
447 'value' => $layout['min'],
448 'prepend' => __( 'Min', 'secure-custom-fields' ),
449 )
450 );
451
452 ?>
453 </li>
454 <li class="acf-fc-meta-max">
455 <?php
456
457 acf_render_field(
458 array(
459 'type' => 'text',
460 'name' => 'max',
461 'prefix' => $layout_prefix,
462 'value' => $layout['max'],
463 'prepend' => __( 'Max', 'secure-custom-fields' ),
464 )
465 );
466
467 ?>
468 </li>
469 </ul>
470 <div class="acf-input-sub">
471 <?php
472
473 // vars
474 $args = array(
475 'fields' => $layout['sub_fields'],
476 'parent' => $field['ID'],
477 'is_subfield' => true,
478 );
479
480 acf_get_view( 'acf-field-group/fields', $args );
481
482 ?>
483 </div>
484 </div>
485 </div>
486 <?php
487 }
488 }
489
490 /**
491 * Renders the field settings used in the "Presentation" tab.
492 *
493 * @since ACF 6.0
494 *
495 * @param array $field The field settings array.
496 * @return void
497 */
498 public function render_field_presentation_settings( $field ) {
499
500 // min
501 acf_render_field_setting(
502 $field,
503 array(
504 'label' => __( 'Minimum Layouts', 'secure-custom-fields' ),
505 'instructions' => '',
506 'type' => 'number',
507 'name' => 'min',
508 )
509 );
510
511 // max
512 acf_render_field_setting(
513 $field,
514 array(
515 'label' => __( 'Maximum Layouts', 'secure-custom-fields' ),
516 'instructions' => '',
517 'type' => 'number',
518 'name' => 'max',
519 )
520 );
521
522 // add new row label
523 acf_render_field_setting(
524 $field,
525 array(
526 'label' => __( 'Button Label', 'secure-custom-fields' ),
527 'instructions' => '',
528 'type' => 'text',
529 'name' => 'button_label',
530 )
531 );
532 }
533
534
535 /**
536 * Filters the $value after it is loaded from the database.
537 *
538 * @since ACF 3.6
539 *
540 * @param mixed $value The value found in the database.
541 * @param mixed $post_id The post_id from which the value was loaded.
542 * @param array $field The field array holding all the field options.
543 * @return $value
544 */
545 public function load_value( $value, $post_id, $field ) {
546 // bail early if no value
547 if ( empty( $value ) || empty( $field['layouts'] ) ) {
548 return $value;
549 }
550
551 $value = acf_get_array( $value );
552 $disabled_layouts = $this->get_disabled_layouts( $post_id, $field );
553 $rows = array();
554 $layouts = array();
555 foreach ( $field['layouts'] as $k => $layout ) {
556 $layouts[ $layout['name'] ] = $layout['sub_fields'];
557 }
558
559 // loop through rows
560 foreach ( $value as $i => $l ) {
561 // If the layout is disabled, prevent it from showing up on the frontend.
562 if ( $this->should_disable_layout( $i, $disabled_layouts ) ) {
563 continue;
564 }
565
566 // append to $values
567 $rows[ $i ] = array();
568 $rows[ $i ]['acf_fc_layout'] = $l;
569
570 // bail early if layout doesn't contain sub fields
571 if ( empty( $layouts[ $l ] ) ) {
572 continue;
573 }
574
575 // get layout
576 $layout = $layouts[ $l ];
577
578 // loop through sub fields
579 foreach ( array_keys( $layout ) as $j ) {
580
581 // get sub field
582 $sub_field = $layout[ $j ];
583
584 // bail early if no name (tab)
585 if ( acf_is_empty( $sub_field['name'] ) ) {
586 continue;
587 }
588
589 // update full name
590 $sub_field['name'] = "{$field['name']}_{$i}_{$sub_field['name']}";
591
592 // get value
593 $sub_value = acf_get_value( $post_id, $sub_field );
594
595 // add value
596 $rows[ $i ][ $sub_field['key'] ] = $sub_value;
597 }
598 }
599
600 return $rows;
601 }
602
603 /**
604 * Checks if a layout should be disabled based on the provided index and disabled layouts.
605 *
606 * @since ACF 6.5
607 *
608 * @param integer|string $layout_index The index of the layout to check.
609 * @param array $disabled_layouts The array of disabled layout indices.
610 * @return boolean
611 */
612 private function should_disable_layout( $layout_index, $disabled_layouts = array() ): bool {
613 // No disabled layouts provided, so no need to disable.
614 if ( ! is_array( $disabled_layouts ) || empty( $disabled_layouts ) ) {
615 return false;
616 }
617
618 // The layout is not in the disabled list, so no need to disable.
619 if ( ! in_array( $layout_index, $disabled_layouts, true ) ) {
620 return false;
621 }
622
623 if ( is_admin() ) {
624 $args = acf_request_args(
625 array(
626 'action' => '',
627 'query' => '',
628 'block' => null,
629 )
630 );
631
632 $wp_block_type = null;
633
634 // If we are dealing with a flexible content field inside a block.
635 if ( ! empty( $args['block'] ) ) {
636 $block_data = json_decode( wp_unslash( $args['block'] ), true );
637
638 if ( JSON_ERROR_NONE === json_last_error() && is_array( $block_data ) && isset( $block_data['name'] ) ) {
639 $registry = WP_Block_Type_Registry::get_instance();
640 $wp_block_type = $registry->get_registered( $block_data['name'] );
641 }
642 }
643
644 // If this is a block preview, disable the layout.
645 if (
646 // Blocks v2 preview check
647 (
648 'acf/ajax/fetch-block' === $args['action'] &&
649 ! empty( $args['query']['preview'] ) &&
650 ( $wp_block_type && isset( $wp_block_type->acf_block_version ) && $wp_block_type->acf_block_version <= 2 )
651 ) ||
652 // Blocks v3 preview check
653 acf_get_data( 'acf_doing_block_preview' )
654 ) {
655 return true;
656 }
657
658 // Editing a layout in the admin, so don't disable it.
659 return false;
660 }
661
662 // The layout has been disabled, and we're on the frontend.
663 return true;
664 }
665
666
667 /**
668 * This filter is applied to the $value after it is loaded from the db and before it is returned to the template
669 *
670 * @type filter
671 * @since ACF 3.6
672 *
673 * @param mixed $value The value which was loaded from the database.
674 * @param mixed $post_id The $post_id from which the value was loaded.
675 * @param array $field The field array holding all the field options.
676 * @param boolean $escape_html Should the field return a HTML safe formatted value.
677 * @return mixed $value The modified value.
678 */
679 public function format_value( $value, $post_id, $field, $escape_html = false ) {
680
681 // bail early if no value
682 if ( empty( $value ) || empty( $field['layouts'] ) ) {
683 return false;
684 }
685
686 // sort layouts into names
687 $layouts = array();
688 foreach ( $field['layouts'] as $k => $layout ) {
689 $layouts[ $layout['name'] ] = $layout['sub_fields'];
690 }
691
692 // loop over rows
693 foreach ( array_keys( $value ) as $i ) {
694
695 // get layout name
696 $l = $value[ $i ]['acf_fc_layout'];
697
698 // bail early if layout doesn't exist
699 if ( empty( $layouts[ $l ] ) ) {
700 continue;
701 }
702
703 // get layout
704 $layout = $layouts[ $l ];
705
706 // loop through sub fields
707 foreach ( array_keys( $layout ) as $j ) {
708
709 // get sub field
710 $sub_field = $layout[ $j ];
711
712 // bail early if no name (tab)
713 if ( acf_is_empty( $sub_field['name'] ) ) {
714 continue;
715 }
716
717 // extract value
718 $sub_value = acf_extract_var( $value[ $i ], $sub_field['key'] );
719
720 // update $sub_field name
721 $sub_field['name'] = "{$field['name']}_{$i}_{$sub_field['name']}";
722
723 // format value
724 $sub_value = acf_format_value( $sub_value, $post_id, $sub_field, $escape_html );
725
726 // append to $row
727 $value[ $i ][ $sub_field['_name'] ] = $sub_value;
728 }
729 }
730
731 // return
732 return $value;
733 }
734
735
736 /**
737 * Validates a flexible content field value.
738 *
739 * @type function
740 * @date 11/02/2014
741 * @since ACF 5.0.0
742 *
743 * @param bool $valid The validation status.
744 * @param mixed $value The value to validate.
745 * @param array $field The field array.
746 * @param string $input The input element's name attribute.
747 * @return mixed The validation result.
748 */
749 public function validate_value( $valid, $value, $field, $input ) {
750 // vars
751 $count = 0;
752
753 // check if is value (may be empty string)
754 if ( is_array( $value ) ) {
755
756 // remove acfcloneindex
757 if ( isset( $value['acfcloneindex'] ) ) {
758 unset( $value['acfcloneindex'] );
759 }
760
761 foreach ( $value as $row_value ) {
762 // Don't count disabled rows;
763 if ( ! empty( $row_value['acf_fc_layout_disabled'] ) ) {
764 continue;
765 }
766 ++$count;
767 }
768 }
769
770 // validate required
771 if ( $field['required'] && ! $count ) {
772 $valid = false;
773 }
774
775 // validate min
776 $min = (int) $field['min'];
777 if ( $min && $count < $min ) {
778
779 // vars
780 $error = __( 'This field requires at least {min} {label} {identifier}', 'secure-custom-fields' );
781 $identifier = _n( 'layout', 'layouts', $min, 'secure-custom-fields' );
782
783 // replace
784 $error = str_replace( '{min}', $min, $error );
785 $error = str_replace( '{label}', '', $error );
786 $error = str_replace( '{identifier}', $identifier, $error );
787
788 // return
789 return $error;
790 }
791
792 // find layouts
793 $layouts = array();
794 foreach ( array_keys( $field['layouts'] ) as $i ) {
795
796 // vars
797 $layout = $field['layouts'][ $i ];
798
799 // add count
800 $layout['count'] = 0;
801
802 // append
803 $layouts[ $layout['name'] ] = $layout;
804 }
805
806 // validate value
807 if ( $count ) {
808
809 // loop rows
810 foreach ( $value as $i => $row ) {
811 // ensure row is an array
812 if ( ! is_array( $row ) ) {
813 continue;
814 }
815
816 if ( ! empty( $row['acf_fc_layout_disabled'] ) ) {
817 continue;
818 }
819
820 // get layout
821 $l = $row['acf_fc_layout'];
822
823 // bail if layout doesn't exist
824 if ( ! isset( $layouts[ $l ] ) ) {
825 continue;
826 }
827
828 // increase count
829 ++$layouts[ $l ]['count'];
830
831 // bail if no sub fields
832 if ( empty( $layouts[ $l ]['sub_fields'] ) ) {
833 continue;
834 }
835
836 // loop sub fields
837 foreach ( $layouts[ $l ]['sub_fields'] as $sub_field ) {
838
839 // get sub field key
840 $k = $sub_field['key'];
841
842 // bail if no value
843 if ( ! isset( $value[ $i ][ $k ] ) ) {
844 continue;
845 }
846
847 // validate
848 acf_validate_value( $value[ $i ][ $k ], $sub_field, "{$input}[{$i}][{$k}]" );
849 }
850 // end loop sub fields
851 }
852 // end loop rows
853 }
854
855 // validate layouts
856 foreach ( $layouts as $layout ) {
857
858 // validate min / max
859 $min = (int) $layout['min'];
860 $count = $layout['count'];
861 $label = $layout['label'];
862
863 if ( $min && $count < $min ) {
864
865 // vars
866 $error = __( 'This field requires at least {min} {label} {identifier}', 'secure-custom-fields' );
867 $identifier = _n( 'layout', 'layouts', $min, 'secure-custom-fields' );
868
869 // replace
870 $error = str_replace( '{min}', $min, $error );
871 $error = str_replace( '{label}', '"' . $label . '"', $error );
872 $error = str_replace( '{identifier}', $identifier, $error );
873
874 // return
875 return $error;
876 }
877 }
878
879 // return
880 return $valid;
881 }
882
883
884 /**
885 * This function will return a specific layout by name from a field
886 *
887 * @since ACF 5.5.8
888 *
889 * @param string $name The layout name.
890 * @param array $field The field to load the layout from.
891 * @return array|false
892 */
893 public function get_layout( $name, $field ) {
894
895 // bail early if no layouts
896 if ( ! isset( $field['layouts'] ) ) {
897 return false;
898 }
899
900 // loop
901 foreach ( $field['layouts'] as $layout ) {
902
903 // match
904 if ( $layout['name'] === $name ) {
905 return $layout;
906 }
907 }
908
909 // return
910 return false;
911 }
912
913 /**
914 * Retrieves layout meta for the Flexible Content field saved to the provided post.
915 *
916 * @since ACF 6.5
917 *
918 * @param integer|string $post_id The ID of the post being edited.
919 * @param array $field The Flexible Content field array.
920 * @return array
921 */
922 public function get_layout_meta( $post_id, $field ) {
923 $field_name = $field['name'];
924
925 // Enables compatibility with nested Flexible Content fields during render.
926 if ( ! empty( $field['_prepare'] ) ) {
927 $field_name = acf_get_field_type( 'repeater' )->get_field_name_from_input_name( $field_name );
928 }
929
930 // Bail early if we don't have a field name to check.
931 if ( empty( $field_name ) ) {
932 return array();
933 }
934
935 // Return the cached meta if we have it.
936 $cache_key = "$post_id:$field_name";
937 if ( ! empty( $this->layout_meta[ $cache_key ] ) ) {
938 return $this->layout_meta[ $cache_key ];
939 }
940
941 $layout_meta = acf_get_metadata_by_field(
942 $post_id,
943 array(
944 'name' => '_' . $field_name . '_layout_meta',
945 )
946 );
947
948 if ( empty( $layout_meta ) || ! is_array( $layout_meta ) ) {
949 return array();
950 }
951
952 $this->layout_meta[ $cache_key ] = $layout_meta;
953
954 return $this->layout_meta[ $cache_key ];
955 }
956
957 /**
958 * Returns an array of layouts that have been disabled for the current field.
959 *
960 * @since ACF 6.5
961 *
962 * @param integer|string $post_id The ID of the post being edited.
963 * @param array $field The Flexible Content field array.
964 * @return array
965 */
966 public function get_disabled_layouts( $post_id, $field ): array {
967 $layout_meta = $this->get_layout_meta( $post_id, $field );
968
969 if ( empty( $layout_meta['disabled'] ) || ! is_array( $layout_meta['disabled'] ) ) {
970 return array();
971 }
972
973 return $layout_meta['disabled'];
974 }
975
976 /**
977 * Returns an array of layouts that have been renamed for the current field.
978 *
979 * @since ACF 6.5
980 *
981 * @param integer|string $post_id The ID of the post being edited.
982 * @param array $field The Flexible Content field array.
983 * @return array
984 */
985 public function get_renamed_layouts( $post_id, $field ): array {
986 $layout_meta = $this->get_layout_meta( $post_id, $field );
987
988 if ( empty( $layout_meta['renamed'] ) || ! is_array( $layout_meta['renamed'] ) ) {
989 return array();
990 }
991
992 return $layout_meta['renamed'];
993 }
994
995 /**
996 * This function will delete a value row
997 *
998 * @since ACF 5.5.8
999 *
1000 * @param integer $i The index of the row to delete.
1001 * @param array $field The field array containing all settings.
1002 * @param mixed $post_id The post ID where the value is saved.
1003 * @return boolean
1004 */
1005 public function delete_row( $i, $field, $post_id ) {
1006
1007 // vars
1008 $value = acf_get_metadata_by_field( $post_id, $field );
1009
1010 // bail early if no value
1011 if ( ! is_array( $value ) || ! isset( $value[ $i ] ) ) {
1012 return false;
1013 }
1014
1015 // get layout
1016 $layout = $this->get_layout( $value[ $i ], $field );
1017
1018 // bail early if no layout
1019 if ( ! $layout || empty( $layout['sub_fields'] ) ) {
1020 return false;
1021 }
1022
1023 // loop
1024 foreach ( $layout['sub_fields'] as $sub_field ) {
1025
1026 // modify name for delete
1027 $sub_field['name'] = "{$field['name']}_{$i}_{$sub_field['name']}";
1028
1029 // delete value
1030 acf_delete_value( $post_id, $sub_field );
1031 }
1032
1033 // return
1034 return true;
1035 }
1036
1037 /**
1038 * This function will update a value row
1039 *
1040 * @since ACF 5.5.8
1041 *
1042 * @param array $row The row array to update.
1043 * @param integer $i The index of the row to update.
1044 * @param array $field The field array containing all settings.
1045 * @param mixed $post_id The post ID where the value is saved.
1046 * @return boolean
1047 */
1048 public function update_row( $row, $i, $field, $post_id ) {
1049 // bail early if no layout reference
1050 if ( ! is_array( $row ) || ! isset( $row['acf_fc_layout'] ) ) {
1051 return false;
1052 }
1053
1054 // get layout
1055 $layout = $this->get_layout( $row['acf_fc_layout'], $field );
1056
1057 // bail early if no layout
1058 if ( ! $layout || empty( $layout['sub_fields'] ) ) {
1059 return false;
1060 }
1061
1062 foreach ( $layout['sub_fields'] as $sub_field ) {
1063 $value = null;
1064
1065 if ( array_key_exists( $sub_field['key'], $row ) ) {
1066 $value = $row[ $sub_field['key'] ];
1067 } elseif ( array_key_exists( $sub_field['name'], $row ) ) {
1068 $value = $row[ $sub_field['name'] ];
1069 } else {
1070 // Value does not exist.
1071 continue;
1072 }
1073
1074 // modify name for save
1075 $sub_field['name'] = "{$field['name']}_{$i}_{$sub_field['name']}";
1076
1077 // update field
1078 acf_update_value( $value, $post_id, $sub_field );
1079 }
1080
1081 return true;
1082 }
1083
1084 /**
1085 * Filters the $value before it is updated in the database.
1086 *
1087 * @since ACF 3.6
1088 *
1089 * @param mixed $value The value which will be saved in the database.
1090 * @param mixed $post_id The post_id of which the value will be saved.
1091 * @param array $field The field array holding all the field options.
1092 * @return mixed $value The modified value
1093 */
1094 public function update_value( $value, $post_id, $field ) {
1095
1096 // Bail early if no layouts or field name.
1097 if ( empty( $field['layouts'] ) || empty( $field['name'] ) ) {
1098 return $value;
1099 }
1100
1101 // vars
1102 $new_value = array();
1103 $disabled_layouts = array();
1104 $renamed_layouts = array();
1105 $old_value = acf_get_metadata_by_field( $post_id, $field );
1106 $old_value = is_array( $old_value ) ? $old_value : array();
1107
1108 // update
1109 if ( ! empty( $value ) ) {
1110 $i = -1;
1111
1112 // remove acfcloneindex
1113 if ( isset( $value['acfcloneindex'] ) ) {
1114 unset( $value['acfcloneindex'] );
1115 }
1116
1117 // loop through rows
1118 foreach ( $value as $row ) {
1119 ++$i;
1120
1121 // bail early if no layout reference
1122 if ( ! is_array( $row ) || ! isset( $row['acf_fc_layout'] ) ) {
1123 continue;
1124 }
1125
1126 // delete old row if layout has changed
1127 if ( isset( $old_value[ $i ] ) && $old_value[ $i ] !== $row['acf_fc_layout'] ) {
1128 $this->delete_row( $i, $field, $post_id );
1129 }
1130
1131 if ( ! empty( $row['acf_fc_layout_disabled'] ) ) {
1132 $disabled_layouts[] = $i;
1133 }
1134 unset( $row['acf_fc_layout_disabled'] );
1135
1136 if ( ! empty( $row['acf_fc_layout_custom_label'] ) ) {
1137 $renamed_layouts[ $i ] = $row['acf_fc_layout_custom_label'];
1138 }
1139 unset( $row['acf_fc_layout_custom_label'] );
1140
1141 // update row
1142 $this->update_row( $row, $i, $field, $post_id );
1143
1144 // append to order
1145 $new_value[] = $row['acf_fc_layout'];
1146 }
1147 }
1148
1149 // vars
1150 $old_count = empty( $old_value ) ? 0 : count( $old_value );
1151 $new_count = empty( $new_value ) ? 0 : count( $new_value );
1152
1153 // Update layout meta.
1154 acf_update_metadata_by_field(
1155 $post_id,
1156 array(
1157 'name' => '_' . $field['name'] . '_layout_meta',
1158 ),
1159 array(
1160 'disabled' => $disabled_layouts,
1161 'renamed' => $renamed_layouts,
1162 )
1163 );
1164
1165 // remove old rows
1166 if ( $old_count > $new_count ) {
1167
1168 // loop
1169 for ( $i = $new_count; $i < $old_count; $i++ ) {
1170 $this->delete_row( $i, $field, $post_id );
1171 }
1172 }
1173
1174 // save false for empty value
1175 if ( empty( $new_value ) ) {
1176 $new_value = '';
1177 }
1178
1179 // return
1180 return $new_value;
1181 }
1182
1183
1184 /**
1185 * Deletes a layout from a flexible content field.
1186 *
1187 * @type function
1188 * @date 1/07/2015
1189 * @since ACF 5.2.3
1190 *
1191 * @param int $post_id The post ID.
1192 * @param string $key The field key.
1193 * @param array $field The field array.
1194 * @return void
1195 */
1196 public function delete_value( $post_id, $key, $field ) {
1197
1198 // Delete layout meta (disabled and renamed layouts).
1199 acf_delete_metadata_by_field(
1200 $post_id,
1201 array(
1202 'name' => '_' . $field['name'] . '_layout_meta',
1203 )
1204 );
1205
1206 // vars
1207 $old_value = acf_get_metadata_by_field( $post_id, $field );
1208 $old_value = is_array( $old_value ) ? $old_value : array();
1209
1210 // bail early if no rows or no sub fields
1211 if ( empty( $old_value ) ) {
1212 return;
1213 }
1214
1215 // loop
1216 foreach ( array_keys( $old_value ) as $i ) {
1217 $this->delete_row( $i, $field, $post_id );
1218 }
1219 }
1220
1221
1222 /**
1223 * This filter is applied to the $field before it is saved to the database
1224 *
1225 * @type filter
1226 * @since ACF 3.6
1227 *
1228 * @param array $field The field array holding all the field options.
1229 * @return array $field The modified field
1230 */
1231 public function update_field( $field ) {
1232
1233 // loop
1234 if ( ! empty( $field['layouts'] ) ) {
1235 foreach ( $field['layouts'] as &$layout ) {
1236 unset( $layout['sub_fields'] );
1237 }
1238 }
1239
1240 // return
1241 return $field;
1242 }
1243
1244
1245 /**
1246 * Deletes a field and its sub fields.
1247 *
1248 * @type function
1249 * @date 4/04/2014
1250 * @since ACF 5.0.0
1251 *
1252 * @param array $field The field array to delete.
1253 * @return void
1254 */
1255 public function delete_field( $field ) {
1256
1257 if ( ! empty( $field['layouts'] ) ) {
1258
1259 // loop through layouts
1260 foreach ( $field['layouts'] as $layout ) {
1261
1262 // loop through sub fields
1263 if ( ! empty( $layout['sub_fields'] ) ) {
1264 foreach ( $layout['sub_fields'] as $sub_field ) {
1265 acf_delete_field( $sub_field['ID'] );
1266 }
1267 // foreach
1268 }
1269 // if
1270 }
1271 // foreach
1272 }
1273 // if
1274 }
1275
1276
1277 /**
1278 * This filter is applied to the $field before it is duplicated and saved to the database
1279 *
1280 * @type filter
1281 * @date 23/01/13
1282 * @since ACF 3.6
1283 *
1284 * @param array $field The field array holding all the field options.
1285 * @return array The modified field.
1286 */
1287 public function duplicate_field( $field ) {
1288
1289 // vars
1290 $sub_fields = array();
1291
1292 if ( ! empty( $field['layouts'] ) ) {
1293
1294 // loop through layouts
1295 foreach ( $field['layouts'] as $layout ) {
1296
1297 // extract sub fields
1298 $extra = acf_extract_var( $layout, 'sub_fields' );
1299
1300 // merge
1301 if ( ! empty( $extra ) ) {
1302 $sub_fields = array_merge( $sub_fields, $extra );
1303 }
1304 }
1305 // foreach
1306 }
1307
1308 // save field to get ID
1309 $field = acf_update_field( $field );
1310
1311 // duplicate sub fields
1312 acf_duplicate_fields( $sub_fields, $field['ID'] );
1313
1314 return $field;
1315 }
1316
1317
1318 /**
1319 * Output the layout title for an AJAX response.
1320 *
1321 * @since ACF 5.3.2
1322 */
1323 public function ajax_layout_title() {
1324
1325 $options = acf_parse_args(
1326 $_POST, // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Verified below via acf_verify_ajax.
1327 array(
1328 'post_id' => 0,
1329 'i' => 0,
1330 'field_key' => '',
1331 'nonce' => '',
1332 'layout' => '',
1333 'value' => array(),
1334 )
1335 );
1336
1337 if ( ! acf_verify_ajax( $options['nonce'], $options['field_key'], true, 'flexible_content' ) ) {
1338 die();
1339 }
1340
1341 // load field
1342 $field = acf_get_field( $options['field_key'] );
1343 if ( ! $field ) {
1344 die();
1345 }
1346
1347 // vars
1348 $layout = $this->get_layout( $options['layout'], $field );
1349 if ( ! $layout ) {
1350 die();
1351 }
1352
1353 // title
1354 $title = $this->get_layout_title( $field, $layout, $options['i'], $options['value'] );
1355
1356 // echo
1357 echo acf_esc_html( $title );
1358 die;
1359 }
1360
1361
1362 /**
1363 * Get a layout title for a field.
1364 *
1365 * @param array $field The field array.
1366 * @param array $layout The layout array.
1367 * @param integer $i The order number of the layout.
1368 * @param array $value The value of the layout.
1369 * @return string The layout title, optionally filtered.
1370 */
1371 public function get_layout_title( $field, $layout, $i, $value ) {
1372 $layout = new Layout( $field, $layout, $i, $value );
1373 return $layout->get_title();
1374 }
1375
1376
1377 /**
1378 * Updates clone field settings based on the original field.
1379 *
1380 * @type function
1381 * @date 28/06/2016
1382 * @since ACF 5.3.8
1383 *
1384 * @param array $field The field array.
1385 * @param array $clone_field The clone field array.
1386 * @return array The modified field.
1387 */
1388 public function clone_any_field( $field, $clone_field ) {
1389
1390 // remove parent_layout
1391 // - allows a sub field to be rendered as a normal field
1392 unset( $field['parent_layout'] );
1393
1394 // attempt to merger parent_layout
1395 if ( isset( $clone_field['parent_layout'] ) ) {
1396 $field['parent_layout'] = $clone_field['parent_layout'];
1397 }
1398
1399 // return
1400 return $field;
1401 }
1402
1403
1404 /**
1405 * Handles preparing the field for export.
1406 *
1407 * @since ACF 5.0.0
1408 *
1409 * @param array $field The whole field array.
1410 * @return array The export ready field array.
1411 */
1412 public function prepare_field_for_export( $field ) {
1413
1414 // loop
1415 if ( ! empty( $field['layouts'] ) ) {
1416 foreach ( $field['layouts'] as &$layout ) {
1417 $layout['sub_fields'] = acf_prepare_fields_for_export( $layout['sub_fields'] );
1418 }
1419 }
1420
1421 // return
1422 return $field;
1423 }
1424
1425 /**
1426 * Prepares any field for export by removing unnecessary data.
1427 *
1428 * @since ACF 5.0.0
1429 *
1430 * @param array $field The field array.
1431 * @return array The prepared field.
1432 */
1433 public function prepare_any_field_for_export( $field ) {
1434
1435 // remove parent_layout
1436 unset( $field['parent_layout'] );
1437
1438 // return
1439 return $field;
1440 }
1441
1442
1443 /**
1444 * Prepares the field for import.
1445 *
1446 * @type function
1447 * @date 11/03/2014
1448 * @since ACF 5.0.0
1449 *
1450 * @param array $field The field array to prepare.
1451 * @return array The prepared field.
1452 */
1453 public function prepare_field_for_import( $field ) {
1454
1455 // Bail early if no layouts
1456 if ( empty( $field['layouts'] ) ) {
1457 return $field;
1458 }
1459
1460 // Storage for extracted fields.
1461 $extra = array();
1462
1463 // Loop over layouts.
1464 foreach ( $field['layouts'] as &$layout ) {
1465
1466 // Ensure layout is valid.
1467 $layout = $this->get_valid_layout( $layout );
1468
1469 // Extract sub fields.
1470 $sub_fields = acf_extract_var( $layout, 'sub_fields' );
1471
1472 // Modify and append sub fields to $extra.
1473 if ( $sub_fields ) {
1474 foreach ( $sub_fields as $i => $sub_field ) {
1475
1476 // Update attributes
1477 $sub_field['parent'] = $field['key'];
1478 $sub_field['parent_layout'] = $layout['key'];
1479 $sub_field['menu_order'] = $i;
1480
1481 // Append to extra.
1482 $extra[] = $sub_field;
1483 }
1484 }
1485 }
1486
1487 // Merge extra sub fields.
1488 if ( $extra ) {
1489 array_unshift( $extra, $field );
1490 return $extra;
1491 }
1492
1493 // Return field.
1494 return $field;
1495 }
1496
1497
1498 /**
1499 * This function will add compatibility for the 'column_width' setting.
1500 *
1501 * Unsure of reason for function name.
1502 *
1503 * @type function
1504 * @date 30/1/17
1505 * @since ACF 5.5.6
1506 *
1507 * @param array $field Adds column width.
1508 * @return array
1509 */
1510 public function validate_any_field( $field ) {
1511
1512 // width has changed
1513 if ( isset( $field['column_width'] ) ) {
1514 $field['wrapper']['width'] = acf_extract_var( $field, 'column_width' );
1515 }
1516
1517 // return
1518 return $field;
1519 }
1520
1521
1522 /**
1523 * This function will translate field settings
1524 *
1525 * @type function
1526 * @date 8/03/2016
1527 * @since ACF 5.3.2
1528 *
1529 * @param array $field The field array containing translation strings.
1530 * @return array The translated field array.
1531 */
1532 public function translate_field( $field ) {
1533
1534 // translate
1535 $field['button_label'] = acf_translate( $field['button_label'] );
1536
1537 // loop
1538 if ( ! empty( $field['layouts'] ) ) {
1539 foreach ( $field['layouts'] as &$layout ) {
1540 $layout['label'] = acf_translate( $layout['label'] );
1541 }
1542 }
1543
1544 // return
1545 return $field;
1546 }
1547
1548 /**
1549 * Additional validation for the flexible content field when submitted via REST.
1550 *
1551 * @param bool $valid The current validity boolean.
1552 * @param mixed $value The value of the field being validated.
1553 * @param array $field The field array containing all settings.
1554 * @return bool|WP_Error Returns true if valid, WP_Error if validation fails.
1555 */
1556 public function validate_rest_value( $valid, $value, $field ) {
1557 $param = sprintf( '%s[%s]', $field['prefix'], $field['name'] );
1558 $data = array(
1559 'param' => $param,
1560 'value' => $value,
1561 );
1562
1563 if ( ! is_array( $value ) && is_null( $value ) ) {
1564 /* translators: 1: Submitted value */
1565 $error = sprintf( __( '%s must be of type array or null.', 'secure-custom-fields' ), $param );
1566 return new WP_Error( 'rest_invalid_param', $error, $param );
1567 }
1568
1569 $layouts_to_update = array_count_values( array_column( $value, 'acf_fc_layout' ) );
1570
1571 foreach ( $field['layouts'] as $layout ) {
1572 $num_layouts = isset( $layouts_to_update[ $layout['name'] ] ) ? $layouts_to_update[ $layout['name'] ] : 0;
1573
1574 if ( '' !== $layout['min'] && $num_layouts < (int) $layout['min'] ) {
1575 $error = sprintf(
1576 /* translators: 1: Field name, 2: Minimum number of layouts, 3: Layout name */
1577 _n(
1578 '%1$s must contain at least %2$s %3$s layout.',
1579 '%1$s must contain at least %2$s %3$s layouts.',
1580 $layout['min'],
1581 'secure-custom-fields'
1582 ),
1583 $param,
1584 number_format_i18n( $layout['min'] ),
1585 $layout['name']
1586 );
1587
1588 return new WP_Error( 'rest_invalid_param', $error, $data );
1589 }
1590
1591 if ( '' !== $layout['max'] && $num_layouts > (int) $layout['max'] ) {
1592 $error = sprintf(
1593 /* translators: 1: field name, 2: minimum number of layouts, 3: layout name */
1594 _n(
1595 '%1$s must contain at most %2$s %3$s layout.',
1596 '%1$s must contain at most %2$s %3$s layouts.',
1597 $layout['max'],
1598 'secure-custom-fields'
1599 ),
1600 $param,
1601 number_format_i18n( $layout['max'] ),
1602 $layout['name']
1603 );
1604
1605 return new WP_Error( 'rest_invalid_param', $error, $data );
1606 }
1607 }
1608
1609 return $valid;
1610 }
1611
1612 /**
1613 * Return the schema array for the REST API.
1614 *
1615 * @param array $field The field array containing all settings.
1616 * @return array The schema array for REST API.
1617 */
1618 public function get_rest_schema( array $field ) {
1619 $schema = array(
1620 'type' => array( 'array', 'null' ),
1621 'required' => ! empty( $field['required'] ),
1622 'items' => array(
1623 'oneOf' => array(),
1624 ),
1625 );
1626
1627 // Loop through layouts building up a schema for each.
1628 foreach ( $field['layouts'] as $layout ) {
1629 $layout_schema = array(
1630 'type' => 'object',
1631 'properties' => array(
1632 'acf_fc_layout' => array(
1633 'type' => 'string',
1634 'required' => true,
1635 // By using a pattern match against the layout name, data sent in must match an available
1636 // layout on the flexible field. If it doesn't, a 400 Bad Request response will result.
1637 'pattern' => '^' . $layout['name'] . '$',
1638 ),
1639 ),
1640 );
1641
1642 foreach ( $layout['sub_fields'] as $sub_field ) {
1643 $sub_field_schema = acf_get_field_rest_schema( $sub_field );
1644 if ( $sub_field_schema ) {
1645 $layout_schema['properties'][ $sub_field['name'] ] = $sub_field_schema;
1646 }
1647 }
1648
1649 $schema['items']['oneOf'][] = $layout_schema;
1650 }
1651
1652 if ( ! empty( $field['min'] ) ) {
1653 $schema['minItems'] = (int) $field['min'];
1654 }
1655
1656 if ( ! empty( $field['max'] ) ) {
1657 $schema['maxItems'] = (int) $field['max'];
1658 }
1659
1660 return $schema;
1661 }
1662
1663 /**
1664 * Apply basic formatting to prepare the value for default REST output.
1665 *
1666 * @param mixed $value The field value to format.
1667 * @param integer|string $post_id The post ID where the value is saved.
1668 * @param array $field The field array containing all settings.
1669 * @return array|mixed The formatted value.
1670 */
1671 public function format_value_for_rest( $value, $post_id, array $field ) {
1672 if ( empty( $value ) || ! is_array( $value ) || empty( $field['layouts'] ) ) {
1673 return null;
1674 }
1675
1676 // Create a map of layout sub fields mapped to layout names.
1677 foreach ( $field['layouts'] as $layout ) {
1678 $layouts[ $layout['name'] ] = $layout['sub_fields'];
1679 }
1680
1681 // Loop through each layout and within that, each sub field to process sub fields individually.
1682 foreach ( $value as &$layout ) {
1683 $name = $layout['acf_fc_layout'];
1684
1685 if ( empty( $layouts[ $name ] ) ) {
1686 continue;
1687 }
1688
1689 foreach ( $layouts[ $name ] as $sub_field ) {
1690
1691 // Bail early if the field has no name (tab).
1692 if ( acf_is_empty( $sub_field['name'] ) ) {
1693 continue;
1694 }
1695
1696 // Extract the sub field 'field_key'=>'value' pair from the $layout and format it.
1697 $sub_value = acf_extract_var( $layout, $sub_field['key'] );
1698 $sub_value = acf_format_value_for_rest( $sub_value, $post_id, $sub_field );
1699
1700 // Add the sub field value back to the $layout but mapped to the field name instead
1701 // of the key reference.
1702 $layout[ $sub_field['name'] ] = $sub_value;
1703 }
1704 }
1705
1706 return $value;
1707 }
1708 }
1709
1710
1711 // initialize
1712 acf_register_field_type( 'acf_field_flexible_content' );
1713 endif; // class_exists check
1714