PluginProbe ʕ •ᴥ•ʔ
Secure Custom Fields / 6.4.1-beta5
Secure Custom Fields v6.4.1-beta5
6.9.5 6.9.4 6.9.3 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-clone.php
secure-custom-fields / includes / fields Last commit date
class-acf-field-accordion.php 1 year ago class-acf-field-button-group.php 1 year ago class-acf-field-checkbox.php 1 year ago class-acf-field-clone.php 1 year ago class-acf-field-color_picker.php 1 year ago class-acf-field-date_picker.php 1 year ago class-acf-field-date_time_picker.php 1 year ago class-acf-field-email.php 1 year ago class-acf-field-file.php 1 year ago class-acf-field-flexible-content.php 1 year ago class-acf-field-gallery.php 1 year ago class-acf-field-google-map.php 1 year ago class-acf-field-group.php 1 year ago class-acf-field-icon_picker.php 1 year ago class-acf-field-image.php 1 year ago class-acf-field-link.php 1 year ago class-acf-field-message.php 1 year ago class-acf-field-number.php 1 year ago class-acf-field-oembed.php 1 year ago class-acf-field-output.php 1 year ago class-acf-field-page_link.php 1 year ago class-acf-field-password.php 1 year ago class-acf-field-post_object.php 1 year ago class-acf-field-radio.php 1 year ago class-acf-field-range.php 1 year ago class-acf-field-relationship.php 1 year ago class-acf-field-repeater.php 1 year ago class-acf-field-select.php 1 year ago class-acf-field-separator.php 1 year ago class-acf-field-tab.php 1 year ago class-acf-field-taxonomy.php 1 year ago class-acf-field-text.php 1 year ago class-acf-field-textarea.php 1 year ago class-acf-field-time_picker.php 1 year ago class-acf-field-true_false.php 1 year ago class-acf-field-url.php 1 year ago class-acf-field-user.php 1 year ago class-acf-field-wysiwyg.php 1 year ago class-acf-field.php 1 year ago class-acf-repeater-table.php 1 year ago index.php 1 year ago
class-acf-field-clone.php
1113 lines
1 <?php
2 /**
3 * Clone Field Class
4 *
5 * This class handles the clone field type, which allows users to select and display existing fields.
6 *
7 * @package wordpress/secure-custom-fields
8 * @since 5.0.0
9 */
10
11 // phpcs:disable PEAR.NamingConventions.ValidClassName
12 if ( ! class_exists( 'acf_field_clone' ) ) :
13 /**
14 * Class acf_field_clone
15 *
16 * Handles the functionality for the clone field type.
17 *
18 * @since 5.0.0
19 */
20 class acf_field_clone extends acf_field {
21 /**
22 * Initialize the field type.
23 *
24 * @type function
25 * @date 5/03/2014
26 * @since ACF 5.0
27 *
28 * @return void
29 */
30 public function initialize() {
31
32 // vars
33 $this->name = 'clone';
34 $this->label = _x( 'Clone', 'noun', 'secure-custom-fields' );
35 $this->category = 'layout';
36 $this->description = __( 'Allows you to select and display existing fields. It does not duplicate any fields in the database, but loads and displays the selected fields at run-time. The Clone field can either replace itself with the selected fields or display the selected fields as a group of subfields.', 'secure-custom-fields' );
37 $this->preview_image = acf_get_url() . '/assets/images/field-type-previews/field-preview-clone.png';
38 $this->doc_url = 'https://www.advancedcustomfields.com/resources/clone/';
39 $this->tutorial_url = 'https://www.advancedcustomfields.com/resources/how-to-use-the-clone-field/';
40 $this->pro = true;
41 $this->supports = array( 'bindings' => false );
42 $this->defaults = array(
43 'clone' => '',
44 'prefix_label' => 0,
45 'prefix_name' => 0,
46 'display' => 'seamless',
47 'layout' => 'block',
48 );
49 $this->cloning = array();
50 $this->have_rows = 'single';
51
52 // register filter
53 acf_enable_filter( 'clone' );
54
55 // ajax
56 add_action( 'wp_ajax_acf/fields/clone/query', array( $this, 'ajax_query' ) );
57
58 // filters
59 add_filter( 'acf/get_fields', array( $this, 'acf_get_fields' ), 5, 2 );
60 add_filter( 'acf/prepare_field', array( $this, 'acf_prepare_field' ), 10, 1 );
61 add_filter( 'acf/clone_field', array( $this, 'acf_clone_field' ), 10, 2 );
62 }
63
64
65 /**
66 * Returns true if ACF local functionality is enabled.
67 *
68 * @type function
69 * @date 14/07/2016
70 * @since ACF 5.4.0
71 *
72 * @return bool True if clone filter is enabled.
73 */
74 public function is_enabled() {
75 return acf_is_filter_enabled( 'clone' );
76 }
77
78
79 /**
80 * Filter applied to the field after it is loaded from the database.
81 *
82 * @type filter
83 * @since ACF 3.6
84 * @date 23/01/13
85 *
86 * @param array $field The field array holding all the field options.
87 * @return array The modified field array.
88 */
89 public function load_field( $field ) {
90
91 // bail early if not enabled
92 if ( ! $this->is_enabled() ) {
93 return $field;
94 }
95
96 // load sub fields
97 // - sub field name's will be modified to include prefix_name settings
98 $field['sub_fields'] = $this->get_cloned_fields( $field );
99
100 // return
101 return $field;
102 }
103
104
105 /**
106 * Hooks into 'acf/get_fields' filter to inject/replace seamless clone fields.
107 *
108 * @param array $fields Field list.
109 * @param array $parent_field Parent field.
110 * @return array Modified field list.
111 */
112 public function acf_get_fields( $fields, $parent_field ) {
113 // bail early if empty.
114 if ( empty( $fields ) ) {
115 return $fields;
116 }
117
118 // bail early if not enabled.
119 if ( ! $this->is_enabled() ) {
120 return $fields;
121 }
122
123 // vars.
124 $i = 0;
125
126 // loop.
127 $count = count( $fields );
128 while ( $i < $count ) {
129
130 // vars.
131 $field = $fields[ $i ];
132
133 // Increment $i.
134 ++$i;
135
136 // Bail early if not a clone field.
137 if ( 'clone' !== $field['type'] ) {
138 continue;
139 }
140
141 // Bail early if not seamless.
142 if ( 'seamless' !== $field['display'] ) {
143 continue;
144 }
145
146 // bail early if sub_fields isn't set or not an array
147 if ( ! isset( $field['sub_fields'] ) || ! is_array( $field['sub_fields'] ) ) {
148 continue;
149 }
150
151 // replace this clone field with sub fields
152 --$i;
153 array_splice( $fields, $i, 1, $field['sub_fields'] );
154 }
155
156 // return
157 return $fields;
158 }
159
160
161 /**
162 * Returns an array of fields for a given clone field.
163 *
164 * @param array $field The clone field array.
165 * @return array Array of cloned fields.
166 */
167 public function get_cloned_fields( $field ) {
168 // vars.
169 $fields = array();
170
171 // bail early if no clone setting.
172 if ( empty( $field['clone'] ) ) {
173 return $fields;
174 }
175
176 // bail early if already cloning this field (avoid infinite looping).
177 if ( isset( $this->cloning[ $field['key'] ] ) ) {
178 return $fields;
179 }
180
181 // update local ref.
182 $this->cloning[ $field['key'] ] = 1;
183
184 // Loop over selectors and load fields.
185 foreach ( $field['clone'] as $selector ) {
186
187 // Field Group selector.
188 if ( acf_is_field_group_key( $selector ) ) {
189 $field_group = acf_get_field_group( $selector );
190 if ( ! $field_group ) {
191 continue;
192 }
193
194 $field_group_fields = acf_get_fields( $field_group );
195 if ( ! $field_group_fields ) {
196 continue;
197 }
198
199 $fields = array_merge( $fields, $field_group_fields );
200
201 // Field selector.
202 } elseif ( acf_is_field_key( $selector ) ) {
203 $fields[] = acf_get_field( $selector );
204 }
205 }
206
207 // field has ve been loaded for this $parent, time to remove cloning ref.
208 unset( $this->cloning[ $field['key'] ] );
209
210 // clear false values (fields that don't exist).
211 $fields = array_filter( $fields );
212
213 // bail early if no sub fields.
214 if ( empty( $fields ) ) {
215 return array();
216 }
217
218 // loop.
219 // run acf_clone_field() on each cloned field to modify name, key, etc.
220 foreach ( array_keys( $fields ) as $i ) {
221 $fields[ $i ] = acf_clone_field( $fields[ $i ], $field );
222 }
223
224 return $fields;
225 }
226
227
228 /**
229 * This function is run when cloning a clone field
230 * Important to run the acf_clone_field function on sub fields to pass on settings such as 'parent_layout'
231 * Do not delete! Removing this logic causes major issues with cloned clone fields within a flexible content layout.
232 *
233 * @param array $field The field being cloned.
234 * @param array $clone_field The clone field.
235 * @return array The modified field.
236 */
237 public function acf_clone_clone_field( $field, $clone_field ) {
238
239 // Modify the $clone_field name.
240 // This seems odd, however, the $clone_field is later passed into the acf_clone_field() function.
241 // Do not delete!
242 // When cloning a clone field, it is important to also change the _name too.
243 // This allows sub clone fields to appear correctly in get_row() row array.
244 if ( $field['prefix_name'] ) {
245 $clone_field['name'] = $field['_name'];
246 $clone_field['_name'] = $field['_name'];
247 }
248
249 // bail early if no sub fields
250 if ( empty( $field['sub_fields'] ) ) {
251 return $field;
252 }
253
254 // loop
255 foreach ( $field['sub_fields'] as &$sub_field ) {
256
257 // clone
258 $sub_field = acf_clone_field( $sub_field, $clone_field );
259 }
260
261 // return
262 return $field;
263 }
264
265
266 /**
267 * Prepares the field for database storage.
268 *
269 * @param array $field The field array.
270 * @return array The prepared field array.
271 */
272 public function prepare_field_for_db( $field ) {
273
274 // bail early if no sub fields
275 if ( empty( $field['sub_fields'] ) ) {
276 return $field;
277 }
278
279 // Bail early if name == _name.
280 // This is a parent clone field and does not require any modification to sub field names.
281 if ( $field['name'] === $field['_name'] ) {
282 return $field;
283 }
284
285 // This is a sub field.
286 // _name = 'my_field' phpcs:ignore
287 // name = 'rep_0_my_field' phpcs:ignore
288 // Modify all sub fields to add 'rep_0_' name prefix (prefix_name setting has already been applied).
289 $length = strlen( $field['_name'] );
290 $prefix = substr( $field['name'], 0, -$length );
291
292 // bail early if _name is not found at the end of name (unknown potential error)
293 if ( $prefix . $field['_name'] !== $field['name'] ) {
294 return $field;
295 }
296
297 // Loop through each sub field and modify its name.
298 foreach ( $field['sub_fields'] as &$sub_field ) {
299 $sub_field['name'] = $prefix . $sub_field['name'];
300 }
301
302 return $field;
303 }
304
305
306 /**
307 * This filter is applied to the $value after it is loaded from the db.
308 *
309 * @param mixed $value The value found in the database.
310 * @param mixed $post_id The post_id from which the value was loaded.
311 * @param array $field The field array holding all the field options.
312 * @return mixed
313 */
314 public function load_value( $value, $post_id, $field ) {
315
316 // bail early if no sub fields
317 if ( empty( $field['sub_fields'] ) ) {
318 return $value;
319 }
320
321 // modify names
322 $field = $this->prepare_field_for_db( $field );
323
324 // load sub fields
325 $value = array();
326
327 // loop
328 foreach ( $field['sub_fields'] as $sub_field ) {
329
330 // add value
331 $value[ $sub_field['key'] ] = acf_get_value( $post_id, $sub_field );
332 }
333
334 // return
335 return $value;
336 }
337
338
339 /**
340 * This filter is appied to the $value after it is loaded from the db and before it is returned to the template
341 *
342 * @type filter
343 * @since ACF 3.6 3.6
344 *
345 * @param mixed $value The value which was loaded from the database.
346 * @param mixed $post_id The $post_id from which the value was loaded.
347 * @param array $field The field array holding all the field options.
348 * @param boolean $escape_html Should the field return a HTML safe formatted value.
349 * @return mixed $value The modified value.
350 */
351 public function format_value( $value, $post_id, $field, $escape_html = false ) {
352
353 // bail early if no value
354 if ( empty( $value ) ) {
355 return false;
356 }
357
358 // modify names
359 $field = $this->prepare_field_for_db( $field );
360
361 // loop
362 foreach ( $field['sub_fields'] as $sub_field ) {
363
364 // extract value
365 $sub_value = acf_extract_var( $value, $sub_field['key'] );
366
367 // format value
368 $sub_value = acf_format_value( $sub_value, $post_id, $sub_field, $escape_html );
369
370 // append to $row
371 $value[ $sub_field['__name'] ] = $sub_value;
372 }
373
374 // return
375 return $value;
376 }
377
378 /**
379 * Formats the value for REST API output.
380 *
381 * @param mixed $value The field value.
382 * @param string|integer $post_id The post ID.
383 * @param array $field The field array.
384 * @return mixed The formatted value.
385 */
386 public function format_value_for_rest( $value, $post_id, array $field ) {
387 if ( empty( $value ) || ! is_array( $value ) ) {
388 return $value;
389 }
390
391 if ( ! is_array( $field ) || ! isset( $field['sub_fields'] ) || ! is_array( $field['sub_fields'] ) ) {
392 return $value;
393 }
394
395 // Loop through each row and within that, each sub field to process sub fields individually.
396 foreach ( $field['sub_fields'] as $sub_field ) {
397
398 // Extract the sub field 'field_key'=>'value' pair from the $value and format it.
399 $sub_value = acf_extract_var( $value, $sub_field['key'] );
400 $sub_value = acf_format_value_for_rest( $sub_value, $post_id, $sub_field );
401
402 // Add the sub field value back to the $value but mapped to the field name instead
403 // of the key reference.
404 $value[ $sub_field['name'] ] = $sub_value;
405 }
406
407 return $value;
408 }
409
410 /**
411 * Updates the field value in the database.
412 *
413 * @param mixed $value The value to save.
414 * @param int $post_id The post ID where the value is saved.
415 * @param array $field The field array.
416 * @return string|null Empty string on success, null on failure.
417 */
418 public function update_value( $value, $post_id, $field ) {
419
420 // bail early if no value
421 if ( ! acf_is_array( $value ) ) {
422 return null;
423 }
424
425 // bail early if no sub fields
426 if ( empty( $field['sub_fields'] ) ) {
427 return null;
428 }
429
430 // modify names
431 $field = $this->prepare_field_for_db( $field );
432
433 // loop
434 foreach ( $field['sub_fields'] as $sub_field ) {
435
436 // vars
437 $v = false;
438
439 // key (backend)
440 if ( isset( $value[ $sub_field['key'] ] ) ) {
441 $v = $value[ $sub_field['key'] ];
442
443 // name (frontend)
444 } elseif ( isset( $value[ $sub_field['_name'] ] ) ) {
445 $v = $value[ $sub_field['_name'] ];
446
447 // empty
448 } else {
449
450 // input is not set (hidden by conditioanl logic)
451 continue;
452 }
453
454 // restore original field key
455 $sub_field = $this->acf_prepare_field( $sub_field );
456
457 // update value
458 acf_update_value( $v, $post_id, $sub_field );
459 }
460
461 // return
462 return '';
463 }
464
465
466 /**
467 * Renders the field input HTML.
468 *
469 * @param array $field The field array.
470 * @return void
471 */
472 public function render_field( $field ) {
473
474 // bail early if no sub fields
475 if ( empty( $field['sub_fields'] ) ) {
476 return;
477 }
478
479 // load values
480 foreach ( $field['sub_fields'] as &$sub_field ) {
481
482 // add value
483 if ( isset( $field['value'][ $sub_field['key'] ] ) ) {
484
485 // this is a normal value
486 $sub_field['value'] = $field['value'][ $sub_field['key'] ];
487 } elseif ( isset( $sub_field['default_value'] ) ) {
488
489 // no value, but this sub field has a default value
490 $sub_field['value'] = $sub_field['default_value'];
491 }
492
493 // update prefix to allow for nested values
494 $sub_field['prefix'] = $field['name'];
495
496 // restore label
497 $sub_field['label'] = $sub_field['__label'];
498
499 // restore required
500 if ( $field['required'] ) {
501 $sub_field['required'] = 0;
502 }
503 }
504
505 // Render the field based on the layout setting.
506 if ( 'table' === $field['layout'] ) {
507 $this->render_field_table( $field );
508 } else {
509 $this->render_field_block( $field );
510 }
511 }
512
513
514 /**
515 * Renders the clone fields in a block layout.
516 *
517 * @param array $field The field array.
518 * @return void
519 */
520 public function render_field_block( $field ) {
521
522 // vars
523 $label_placement = 'block' === $field['layout'] ? 'top' : 'left';
524
525 // html
526 echo '<div class="acf-clone-fields acf-fields -' . esc_attr( $label_placement ) . ' -border">';
527
528 foreach ( $field['sub_fields'] as $sub_field ) {
529 acf_render_field_wrap( $sub_field );
530 }
531
532 echo '</div>';
533 }
534
535
536 /**
537 * Renders the clone fields in a table layout.
538 *
539 * @param array $field The field array.
540 * @return void
541 */
542 public function render_field_table( $field ) {
543 ?>
544 <table class="acf-table">
545 <thead>
546 <tr>
547 <?php
548 foreach ( $field['sub_fields'] as $sub_field ) :
549
550 // Prepare field (allow sub fields to be removed).
551 $sub_field = acf_prepare_field( $sub_field );
552 if ( ! $sub_field ) {
553 continue;
554 }
555
556 // Define attrs.
557 $attrs = array();
558 $attrs['class'] = 'acf-th';
559 $attrs['data-name'] = $sub_field['_name'];
560 $attrs['data-type'] = $sub_field['type'];
561 $attrs['data-key'] = $sub_field['key'];
562
563 if ( $sub_field['wrapper']['width'] ) {
564 $attrs['data-width'] = $sub_field['wrapper']['width'];
565 $attrs['style'] = 'width: ' . $sub_field['wrapper']['width'] . '%;';
566 }
567
568 ?>
569 <th <?php echo acf_esc_attrs( $attrs ); ?>>
570 <?php acf_render_field_label( $sub_field ); ?>
571 <?php acf_render_field_instructions( $sub_field ); ?>
572 </th>
573 <?php endforeach; ?>
574 </tr>
575 </thead>
576 <tbody>
577 <tr class="acf-row">
578 <?php
579
580 foreach ( $field['sub_fields'] as $sub_field ) {
581 acf_render_field_wrap( $sub_field, 'td' );
582 }
583
584 ?>
585 </tr>
586 </tbody>
587 </table>
588 <?php
589 }
590
591
592 /**
593 * Renders the field settings HTML.
594 *
595 * @param array $field The field settings array.
596 * @return void
597 */
598 public function render_field_settings( $field ) {
599
600 // temp enable 'local' to allow .json fields to be displayed
601 acf_enable_filter( 'local' );
602
603 // default_value
604 acf_render_field_setting(
605 $field,
606 array(
607 'label' => __( 'Fields', 'secure-custom-fields' ),
608 'instructions' => __( 'Select one or more fields you wish to clone', 'secure-custom-fields' ),
609 'type' => 'select',
610 'name' => 'clone',
611 'multiple' => 1,
612 'allow_null' => 1,
613 'choices' => $this->get_clone_setting_choices( $field['clone'] ),
614 'ui' => 1,
615 'ajax' => 1,
616 'ajax_action' => 'acf/fields/clone/query',
617 'placeholder' => '',
618 'nonce' => wp_create_nonce( 'acf/fields/clone/query' ),
619 )
620 );
621
622 acf_disable_filter( 'local' );
623
624 // display
625 acf_render_field_setting(
626 $field,
627 array(
628 'label' => __( 'Display', 'secure-custom-fields' ),
629 'instructions' => __( 'Specify the style used to render the clone field', 'secure-custom-fields' ),
630 'type' => 'select',
631 'name' => 'display',
632 'class' => 'setting-display',
633 'choices' => array(
634 'group' => __( 'Group (displays selected fields in a group within this field)', 'secure-custom-fields' ),
635 'seamless' => __( 'Seamless (replaces this field with selected fields)', 'secure-custom-fields' ),
636 ),
637 )
638 );
639
640 // layout
641 acf_render_field_setting(
642 $field,
643 array(
644 'label' => __( 'Layout', 'secure-custom-fields' ),
645 'instructions' => __( 'Specify the style used to render the selected fields', 'secure-custom-fields' ),
646 'type' => 'radio',
647 'name' => 'layout',
648 'layout' => 'horizontal',
649 'choices' => array(
650 'block' => __( 'Block', 'secure-custom-fields' ),
651 'table' => __( 'Table', 'secure-custom-fields' ),
652 'row' => __( 'Row', 'secure-custom-fields' ),
653 ),
654 )
655 );
656
657 // prefix_label
658 /* translators: %s: field label */
659 $instructions = __( 'Labels will be displayed as %s', 'secure-custom-fields' );
660 $instructions = sprintf( $instructions, '<code class="prefix-label-code-1"></code>' );
661 acf_render_field_setting(
662 $field,
663 array(
664 'label' => __( 'Prefix Field Labels', 'secure-custom-fields' ),
665 'instructions' => $instructions,
666 'name' => 'prefix_label',
667 'class' => 'setting-prefix-label',
668 'type' => 'true_false',
669 'ui' => 1,
670 )
671 );
672
673 // prefix_name
674 /* translators: %s: field name */
675 $instructions = __( 'Values will be saved as %s', 'secure-custom-fields' );
676 $instructions = sprintf( $instructions, '<code class="prefix-name-code-1"></code>' );
677 acf_render_field_setting(
678 $field,
679 array(
680 'label' => __( 'Prefix Field Names', 'secure-custom-fields' ),
681 'instructions' => $instructions,
682 'name' => 'prefix_name',
683 'class' => 'setting-prefix-name',
684 'type' => 'true_false',
685 'ui' => 1,
686 )
687 );
688 }
689
690
691 /**
692 * Returns an array of field choices for Select2.
693 *
694 * @param mixed $value The field value.
695 * @return array Array of choices for Select2.
696 */
697 public function get_clone_setting_choices( $value ) {
698
699 // vars
700 $choices = array();
701
702 // bail early if no $value
703 if ( empty( $value ) ) {
704 return $choices;
705 }
706
707 // force value to array
708 $value = acf_get_array( $value );
709
710 // loop
711 foreach ( $value as $v ) {
712 $choices[ $v ] = $this->get_clone_setting_choice( $v );
713 }
714
715 // return
716 return $choices;
717 }
718
719
720 /**
721 * Returns the label for a given clone choice.
722 *
723 * @param mixed $selector The field selector.
724 * @return string The choice label.
725 */
726 public function get_clone_setting_choice( $selector = '' ) {
727
728 // bail early no selector
729 if ( ! $selector ) {
730 return '';
731 }
732
733 // phpcs:disable WordPress.Security.NonceVerification.Missing -- Verified elsewhere.
734 // ajax_fields
735 if ( isset( $_POST['fields'][ $selector ] ) ) {
736 return $this->get_clone_setting_field_choice( acf_sanitize_request_args( wp_unslash( $_POST['fields'][ $selector ] ) ) );
737 }
738 // phpcs:enable WordPress.Security.NonceVerification.Missing
739
740 // field
741 if ( acf_is_field_key( $selector ) ) {
742 return $this->get_clone_setting_field_choice( acf_get_field( $selector ) );
743 }
744
745 // group
746 if ( acf_is_field_group_key( $selector ) ) {
747 return $this->get_clone_setting_group_choice( acf_get_field_group( $selector ) );
748 }
749
750 // return
751 return $selector;
752 }
753
754
755 /**
756 * Returns the text label for a field choice.
757 *
758 * @param array|false $field The field array.
759 * @return string The formatted choice text.
760 */
761 public function get_clone_setting_field_choice( $field ) {
762
763 // bail early if no field
764 if ( ! $field ) {
765 return __( 'Unknown field', 'secure-custom-fields' );
766 }
767
768 // title
769 $title = $field['label'] ? $field['label'] : __( '(no title)', 'secure-custom-fields' );
770
771 // append type
772 $title .= ' (' . $field['type'] . ')';
773
774 // ancestors
775 // - allow for AJAX to send through ancestors count
776 $ancestors = isset( $field['ancestors'] ) ? $field['ancestors'] : count( acf_get_field_ancestors( $field ) );
777 $title = str_repeat( '- ', $ancestors ) . $title;
778
779 // return
780 return $title;
781 }
782
783
784 /**
785 * Returns the text label for a field group choice.
786 *
787 * @param array|false $field_group The field group array.
788 * @return string The formatted choice text.
789 */
790 public function get_clone_setting_group_choice( $field_group ) {
791
792 // bail early if no field group
793 if ( ! $field_group ) {
794 return __( 'Unknown field group', 'secure-custom-fields' );
795 }
796
797 // return
798 /* translators: %s: field group title */
799 return sprintf( __( 'All fields from %s field group', 'secure-custom-fields' ), $field_group['title'] );
800 }
801
802
803 /**
804 * AJAX handler for getting potential fields to clone.
805 *
806 * @since ACF 5.3.8.3.8
807 *
808 * @return void
809 */
810 public function ajax_query() {
811 $nonce = acf_request_arg( 'nonce', '' );
812
813 if ( ! acf_verify_ajax( $nonce, 'acf/fields/clone/query' ) ) {
814 die();
815 }
816
817 // disable field to allow clone fields to appear selectable
818 acf_disable_filter( 'clone' );
819
820 // options
821 $options = acf_parse_args(
822 $_POST,
823 array(
824 'post_id' => 0,
825 'paged' => 0,
826 's' => '',
827 'title' => '',
828 'fields' => array(),
829 )
830 );
831
832 // vars
833 $results = array();
834 $s = false;
835 $i = -1;
836 $limit = 20;
837 $range_start = $limit * ( $options['paged'] - 1 ); // 0, 20, 40
838 $range_end = $range_start + ( $limit - 1 ); // 19, 39, 59
839
840 // search
841 if ( '' !== $options['s'] ) {
842
843 // strip slashes (search may be integer)
844 $s = wp_unslash( strval( $options['s'] ) );
845 }
846
847 // load groups
848 $field_groups = acf_get_field_groups();
849 $field_group = false;
850
851 // bail early if no field groups
852 if ( empty( $field_groups ) ) {
853 die();
854 }
855
856 // move current field group to start
857 foreach ( array_keys( $field_groups ) as $j ) {
858
859 // check ID
860 if ( $field_groups[ $j ]['ID'] !== $options['post_id'] ) {
861 continue;
862 }
863
864 // extract field group and move to start
865 $field_group = acf_extract_var( $field_groups, $j );
866
867 // field group found, stop looking
868 break;
869 }
870
871 // if field group was not found, this is a new field group (not yet saved)
872 if ( ! $field_group ) {
873 $field_group = array(
874 'ID' => $options['post_id'],
875 'title' => $options['title'],
876 'key' => '',
877 );
878 }
879
880 // move current field group to start of list
881 array_unshift( $field_groups, $field_group );
882
883 // loop
884 foreach ( $field_groups as $field_group ) {
885
886 // vars
887 $fields = false;
888 $ignore_s = false;
889 $data = array(
890 'text' => $field_group['title'],
891 'children' => array(),
892 );
893
894 // get fields
895 if ( (int) $field_group['ID'] === (int) $options['post_id'] ) {
896 $fields = $options['fields'];
897 } else {
898 $fields = acf_get_fields( $field_group );
899 $fields = acf_prepare_fields_for_import( $fields );
900 }
901
902 // bail early if no fields
903 if ( ! $fields ) {
904 continue;
905 }
906
907 // show all children for field group search match
908 if ( false !== $s && stripos( $data['text'], $s ) !== false ) {
909 $ignore_s = true;
910 }
911
912 // populate children
913 $children = array();
914 $children[] = $field_group['key'];
915 foreach ( $fields as $field ) {
916 $children[] = $field['key'];
917 }
918
919 // loop
920 foreach ( $children as $child ) {
921
922 // bail early if no key (fake field group or corrupt field)
923 if ( ! $child ) {
924 continue;
925 }
926
927 // vars
928 $text = false;
929
930 // bail early if is search, and $text does not contain $s
931 if ( false !== $s && ! $ignore_s ) {
932
933 // get early
934 $text = $this->get_clone_setting_choice( $child );
935
936 // search
937 if ( stripos( $text, $s ) === false ) {
938 continue;
939 }
940 }
941
942 // $i
943 ++$i;
944
945 // bail early if $i is out of bounds
946 if ( $i < $range_start || $i > $range_end ) {
947 continue;
948 }
949
950 // load text
951 if ( false === $text ) {
952 $text = $this->get_clone_setting_choice( $child );
953 }
954
955 // append
956 $data['children'][] = array(
957 'id' => $child,
958 'text' => $text,
959 );
960 }
961
962 // bail early if no children
963 // - this group contained fields, but none shown on this page
964 if ( empty( $data['children'] ) ) {
965 continue;
966 }
967
968 // append
969 $results[] = $data;
970
971 // end loop if $i is out of bounds
972 // - no need to look further
973 if ( $i > $range_end ) {
974 break;
975 }
976 }
977
978 // return
979 acf_send_ajax_results(
980 array(
981 'results' => $results,
982 'limit' => $limit,
983 )
984 );
985 }
986
987
988 /**
989 * Restores a field's key ready for input.
990 *
991 * @since ACF 5.4.0
992 *
993 * @param array $field The field array.
994 * @return array The modified field array.
995 */
996 public function acf_prepare_field( $field ) {
997
998 // bail early if not cloned
999 if ( empty( $field['_clone'] ) ) {
1000 return $field;
1001 }
1002
1003 // restore key
1004 if ( isset( $field['__key'] ) ) {
1005 $field['key'] = $field['__key'];
1006 }
1007
1008 // return
1009 return $field;
1010 }
1011
1012
1013 /**
1014 * Validates the value of a clone field.
1015 *
1016 * @since ACF 5.0.0
1017 *
1018 * @param bool $valid Whether the value is valid.
1019 * @param mixed $value The field value.
1020 * @param array $field The field array.
1021 * @param string $input The input element's name attribute.
1022 * @return bool Whether the value is valid.
1023 */
1024 public function validate_value( $valid, $value, $field, $input ) {
1025
1026 // bail early if no $value
1027 if ( empty( $value ) ) {
1028 return $valid;
1029 }
1030
1031 // bail early if no sub fields
1032 if ( empty( $field['sub_fields'] ) ) {
1033 return $valid;
1034 }
1035
1036 // loop
1037 foreach ( array_keys( $field['sub_fields'] ) as $i ) {
1038
1039 // get sub field
1040 $sub_field = $field['sub_fields'][ $i ];
1041 $k = $sub_field['key'];
1042
1043 // bail early if value not set (conditional logic?)
1044 if ( ! isset( $value[ $k ] ) ) {
1045 continue;
1046 }
1047
1048 // validate
1049 acf_validate_value( $value[ $k ], $sub_field, "{$input}[{$k}]" );
1050 }
1051
1052 // return
1053 return $valid;
1054 }
1055
1056 /**
1057 * Returns the schema array for the REST API.
1058 *
1059 * @param array $field The field array.
1060 * @return array The schema array for the REST API.
1061 */
1062 public function get_rest_schema( array $field ) {
1063 $schema = array(
1064 'type' => array( 'object', 'null' ),
1065 'required' => ! empty( $field['required'] ) ? array() : false,
1066 'items' => array(
1067 'type' => 'object',
1068 'properties' => array(),
1069 ),
1070 );
1071
1072 foreach ( $field['sub_fields'] as $sub_field ) {
1073 /**
1074 * Field type instance.
1075 *
1076 * @var acf_field $type
1077 */
1078 $type = acf_get_field_type( $sub_field['type'] );
1079
1080 if ( ! $type ) {
1081 continue;
1082 }
1083
1084 $sub_field_schema = $type->get_rest_schema( $sub_field );
1085
1086 // Passing null to nested fields has no effect. Remove this as a possible type to prevent
1087 // confusion in the schema.
1088 $null_type_index = array_search( 'null', $sub_field_schema['type'], true );
1089 if ( false !== $null_type_index ) {
1090 unset( $sub_field_schema['type'][ $null_type_index ] );
1091 }
1092
1093 $schema['items']['properties'][ $sub_field['name'] ] = $sub_field_schema;
1094
1095 /**
1096 * If the clone field itself is marked as required, all subfields are required,
1097 * regardless of the status of the original fields.
1098 */
1099 if ( is_array( $schema['required'] ) ) {
1100 $schema['required'][] = $sub_field['name'];
1101 }
1102 }
1103
1104 return $schema;
1105 }
1106 }
1107
1108
1109 // initialize
1110 acf_register_field_type( 'acf_field_clone' );
1111 endif; // class_exists check
1112
1113 ?>