PluginProbe ʕ •ᴥ•ʔ
Secure Custom Fields / 6.4.1-beta7
Secure Custom Fields v6.4.1-beta7
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
1188 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://developer.wordpress.org/secure-custom-fields/features/fields/clone/';
39 $this->tutorial_url = 'https://developer.wordpress.org/secure-custom-fields/features/fields/clone/clone-tutorial/';
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 * This function is run when cloning a clone field
229 * Important to run the acf_clone_field function on sub fields to pass on settings such as 'parent_layout'
230 *
231 * @type function
232 * @date 28/06/2016
233 * @since 5.3.8
234 *
235 * @param array $field The field array.
236 * @param array $clone_field The clone field array.
237 * @return array $field
238 */
239 public function acf_clone_field( $field, $clone_field ) {
240
241 // bail early if this field is being cloned by some other kind of field (future proof)
242 if ( 'clone' !== $clone_field['type'] ) {
243 return $field;
244 }
245
246 // backup (used later)
247 // - backup only once (cloned clone fields can cause issues)
248 if ( ! isset( $field['__key'] ) ) {
249 $field['__key'] = $field['key'];
250 $field['__name'] = $field['_name'];
251 $field['__label'] = $field['label'];
252 }
253
254 // seamless
255 if ( 'seamless' === $clone_field['display'] ) {
256
257 // modify key
258 // - this will allow sub clone fields to correctly load values for the same cloned field
259 // - the original key will later be restored by acf/prepare_field allowing conditional logic JS to work
260 $field['key'] = $clone_field['key'] . '_' . $field['key'];
261
262 // modify prefix allowing clone field to save sub fields
263 // - only used for parent seamless fields. Block or sub field's prefix will be overriden which also works
264 $field['prefix'] = $clone_field['prefix'] . '[' . $clone_field['key'] . ']';
265
266 // modify parent
267 $field['parent'] = $clone_field['parent'];
268
269 // label_format
270 if ( $clone_field['prefix_label'] ) {
271 $field['label'] = $clone_field['label'] . ' ' . $field['label'];
272 }
273 }
274
275 // prefix_name
276 if ( $clone_field['prefix_name'] ) {
277
278 // modify the field name
279 // - this will allow field to load / save correctly
280 $field['name'] = $clone_field['name'] . '_' . $field['_name'];
281
282 // modify the field _name (orig name)
283 // - this will allow fields to correctly understand the modified field
284 if ( 'seamless' === $clone_field['display'] ) {
285 $field['_name'] = $clone_field['_name'] . '_' . $field['_name'];
286 }
287 }
288
289 // required
290 if ( $clone_field['required'] ) {
291 $field['required'] = 1;
292 }
293
294 // type specific
295 // note: seamless clone fields will not be triggered
296 if ( 'clone' === $field['type'] ) {
297 $field = $this->acf_clone_clone_field( $field, $clone_field );
298 }
299
300 // return
301 return $field;
302 }
303 /**
304 * This function is run when cloning a clone field
305 * Important to run the acf_clone_field function on sub fields to pass on settings such as 'parent_layout'
306 * Do not delete! Removing this logic causes major issues with cloned clone fields within a flexible content layout.
307 *
308 * @param array $field The field being cloned.
309 * @param array $clone_field The clone field.
310 * @return array The modified field.
311 */
312 public function acf_clone_clone_field( $field, $clone_field ) {
313
314 // Modify the $clone_field name.
315 // This seems odd, however, the $clone_field is later passed into the acf_clone_field() function.
316 // Do not delete!
317 // When cloning a clone field, it is important to also change the _name too.
318 // This allows sub clone fields to appear correctly in get_row() row array.
319 if ( $field['prefix_name'] ) {
320 $clone_field['name'] = $field['_name'];
321 $clone_field['_name'] = $field['_name'];
322 }
323
324 // bail early if no sub fields
325 if ( empty( $field['sub_fields'] ) ) {
326 return $field;
327 }
328
329 // loop
330 foreach ( $field['sub_fields'] as &$sub_field ) {
331
332 // clone
333 $sub_field = acf_clone_field( $sub_field, $clone_field );
334 }
335
336 // return
337 return $field;
338 }
339
340
341 /**
342 * Prepares the field for database storage.
343 *
344 * @param array $field The field array.
345 * @return array The prepared field array.
346 */
347 public function prepare_field_for_db( $field ) {
348
349 // bail early if no sub fields
350 if ( empty( $field['sub_fields'] ) ) {
351 return $field;
352 }
353
354 // Bail early if name == _name.
355 // This is a parent clone field and does not require any modification to sub field names.
356 if ( $field['name'] === $field['_name'] ) {
357 return $field;
358 }
359
360 // This is a sub field.
361 // _name = 'my_field' phpcs:ignore
362 // name = 'rep_0_my_field' phpcs:ignore
363 // Modify all sub fields to add 'rep_0_' name prefix (prefix_name setting has already been applied).
364 $length = strlen( $field['_name'] );
365 $prefix = substr( $field['name'], 0, -$length );
366
367 // bail early if _name is not found at the end of name (unknown potential error)
368 if ( $prefix . $field['_name'] !== $field['name'] ) {
369 return $field;
370 }
371
372 // Loop through each sub field and modify its name.
373 foreach ( $field['sub_fields'] as &$sub_field ) {
374 $sub_field['name'] = $prefix . $sub_field['name'];
375 }
376
377 return $field;
378 }
379
380
381 /**
382 * This filter is applied to the $value after it is loaded from the db.
383 *
384 * @param mixed $value The value found in the database.
385 * @param mixed $post_id The post_id from which the value was loaded.
386 * @param array $field The field array holding all the field options.
387 * @return mixed
388 */
389 public function load_value( $value, $post_id, $field ) {
390
391 // bail early if no sub fields
392 if ( empty( $field['sub_fields'] ) ) {
393 return $value;
394 }
395
396 // modify names
397 $field = $this->prepare_field_for_db( $field );
398
399 // load sub fields
400 $value = array();
401
402 // loop
403 foreach ( $field['sub_fields'] as $sub_field ) {
404
405 // add value
406 $value[ $sub_field['key'] ] = acf_get_value( $post_id, $sub_field );
407 }
408
409 // return
410 return $value;
411 }
412
413
414 /**
415 * This filter is appied to the $value after it is loaded from the db and before it is returned to the template
416 *
417 * @type filter
418 * @since ACF 3.6 3.6
419 *
420 * @param mixed $value The value which was loaded from the database.
421 * @param mixed $post_id The $post_id from which the value was loaded.
422 * @param array $field The field array holding all the field options.
423 * @param boolean $escape_html Should the field return a HTML safe formatted value.
424 * @return mixed $value The modified value.
425 */
426 public function format_value( $value, $post_id, $field, $escape_html = false ) {
427
428 // bail early if no value
429 if ( empty( $value ) ) {
430 return false;
431 }
432
433 // modify names
434 $field = $this->prepare_field_for_db( $field );
435
436 // loop
437 foreach ( $field['sub_fields'] as $sub_field ) {
438
439 // extract value
440 $sub_value = acf_extract_var( $value, $sub_field['key'] );
441
442 // format value
443 $sub_value = acf_format_value( $sub_value, $post_id, $sub_field, $escape_html );
444
445 // append to $row
446 $value[ $sub_field['__name'] ] = $sub_value;
447 }
448
449 // return
450 return $value;
451 }
452
453 /**
454 * Formats the value for REST API output.
455 *
456 * @param mixed $value The field value.
457 * @param string|integer $post_id The post ID.
458 * @param array $field The field array.
459 * @return mixed The formatted value.
460 */
461 public function format_value_for_rest( $value, $post_id, array $field ) {
462 if ( empty( $value ) || ! is_array( $value ) ) {
463 return $value;
464 }
465
466 if ( ! is_array( $field ) || ! isset( $field['sub_fields'] ) || ! is_array( $field['sub_fields'] ) ) {
467 return $value;
468 }
469
470 // Loop through each row and within that, each sub field to process sub fields individually.
471 foreach ( $field['sub_fields'] as $sub_field ) {
472
473 // Extract the sub field 'field_key'=>'value' pair from the $value and format it.
474 $sub_value = acf_extract_var( $value, $sub_field['key'] );
475 $sub_value = acf_format_value_for_rest( $sub_value, $post_id, $sub_field );
476
477 // Add the sub field value back to the $value but mapped to the field name instead
478 // of the key reference.
479 $value[ $sub_field['name'] ] = $sub_value;
480 }
481
482 return $value;
483 }
484
485 /**
486 * Updates the field value in the database.
487 *
488 * @param mixed $value The value to save.
489 * @param int $post_id The post ID where the value is saved.
490 * @param array $field The field array.
491 * @return string|null Empty string on success, null on failure.
492 */
493 public function update_value( $value, $post_id, $field ) {
494
495 // bail early if no value
496 if ( ! acf_is_array( $value ) ) {
497 return null;
498 }
499
500 // bail early if no sub fields
501 if ( empty( $field['sub_fields'] ) ) {
502 return null;
503 }
504
505 // modify names
506 $field = $this->prepare_field_for_db( $field );
507
508 // loop
509 foreach ( $field['sub_fields'] as $sub_field ) {
510
511 // vars
512 $v = false;
513
514 // key (backend)
515 if ( isset( $value[ $sub_field['key'] ] ) ) {
516 $v = $value[ $sub_field['key'] ];
517
518 // name (frontend)
519 } elseif ( isset( $value[ $sub_field['_name'] ] ) ) {
520 $v = $value[ $sub_field['_name'] ];
521
522 // empty
523 } else {
524
525 // input is not set (hidden by conditioanl logic)
526 continue;
527 }
528
529 // restore original field key
530 $sub_field = $this->acf_prepare_field( $sub_field );
531
532 // update value
533 acf_update_value( $v, $post_id, $sub_field );
534 }
535
536 // return
537 return '';
538 }
539
540
541 /**
542 * Renders the field input HTML.
543 *
544 * @param array $field The field array.
545 * @return void
546 */
547 public function render_field( $field ) {
548
549 // bail early if no sub fields
550 if ( empty( $field['sub_fields'] ) ) {
551 return;
552 }
553
554 // load values
555 foreach ( $field['sub_fields'] as &$sub_field ) {
556
557 // add value
558 if ( isset( $field['value'][ $sub_field['key'] ] ) ) {
559
560 // this is a normal value
561 $sub_field['value'] = $field['value'][ $sub_field['key'] ];
562 } elseif ( isset( $sub_field['default_value'] ) ) {
563
564 // no value, but this sub field has a default value
565 $sub_field['value'] = $sub_field['default_value'];
566 }
567
568 // update prefix to allow for nested values
569 $sub_field['prefix'] = $field['name'];
570
571 // restore label
572 $sub_field['label'] = $sub_field['__label'];
573
574 // restore required
575 if ( $field['required'] ) {
576 $sub_field['required'] = 0;
577 }
578 }
579
580 // Render the field based on the layout setting.
581 if ( 'table' === $field['layout'] ) {
582 $this->render_field_table( $field );
583 } else {
584 $this->render_field_block( $field );
585 }
586 }
587
588
589 /**
590 * Renders the clone fields in a block layout.
591 *
592 * @param array $field The field array.
593 * @return void
594 */
595 public function render_field_block( $field ) {
596
597 // vars
598 $label_placement = 'block' === $field['layout'] ? 'top' : 'left';
599
600 // html
601 echo '<div class="acf-clone-fields acf-fields -' . esc_attr( $label_placement ) . ' -border">';
602
603 foreach ( $field['sub_fields'] as $sub_field ) {
604 acf_render_field_wrap( $sub_field );
605 }
606
607 echo '</div>';
608 }
609
610
611 /**
612 * Renders the clone fields in a table layout.
613 *
614 * @param array $field The field array.
615 * @return void
616 */
617 public function render_field_table( $field ) {
618 ?>
619 <table class="acf-table">
620 <thead>
621 <tr>
622 <?php
623 foreach ( $field['sub_fields'] as $sub_field ) :
624
625 // Prepare field (allow sub fields to be removed).
626 $sub_field = acf_prepare_field( $sub_field );
627 if ( ! $sub_field ) {
628 continue;
629 }
630
631 // Define attrs.
632 $attrs = array();
633 $attrs['class'] = 'acf-th';
634 $attrs['data-name'] = $sub_field['_name'];
635 $attrs['data-type'] = $sub_field['type'];
636 $attrs['data-key'] = $sub_field['key'];
637
638 if ( $sub_field['wrapper']['width'] ) {
639 $attrs['data-width'] = $sub_field['wrapper']['width'];
640 $attrs['style'] = 'width: ' . $sub_field['wrapper']['width'] . '%;';
641 }
642
643 ?>
644 <th <?php echo acf_esc_attrs( $attrs ); ?>>
645 <?php acf_render_field_label( $sub_field ); ?>
646 <?php acf_render_field_instructions( $sub_field ); ?>
647 </th>
648 <?php endforeach; ?>
649 </tr>
650 </thead>
651 <tbody>
652 <tr class="acf-row">
653 <?php
654
655 foreach ( $field['sub_fields'] as $sub_field ) {
656 acf_render_field_wrap( $sub_field, 'td' );
657 }
658
659 ?>
660 </tr>
661 </tbody>
662 </table>
663 <?php
664 }
665
666
667 /**
668 * Renders the field settings HTML.
669 *
670 * @param array $field The field settings array.
671 * @return void
672 */
673 public function render_field_settings( $field ) {
674
675 // temp enable 'local' to allow .json fields to be displayed
676 acf_enable_filter( 'local' );
677
678 // default_value
679 acf_render_field_setting(
680 $field,
681 array(
682 'label' => __( 'Fields', 'secure-custom-fields' ),
683 'instructions' => __( 'Select one or more fields you wish to clone', 'secure-custom-fields' ),
684 'type' => 'select',
685 'name' => 'clone',
686 'multiple' => 1,
687 'allow_null' => 1,
688 'choices' => $this->get_clone_setting_choices( $field['clone'] ),
689 'ui' => 1,
690 'ajax' => 1,
691 'ajax_action' => 'acf/fields/clone/query',
692 'placeholder' => '',
693 'nonce' => wp_create_nonce( 'acf/fields/clone/query' ),
694 )
695 );
696
697 acf_disable_filter( 'local' );
698
699 // display
700 acf_render_field_setting(
701 $field,
702 array(
703 'label' => __( 'Display', 'secure-custom-fields' ),
704 'instructions' => __( 'Specify the style used to render the clone field', 'secure-custom-fields' ),
705 'type' => 'select',
706 'name' => 'display',
707 'class' => 'setting-display',
708 'choices' => array(
709 'group' => __( 'Group (displays selected fields in a group within this field)', 'secure-custom-fields' ),
710 'seamless' => __( 'Seamless (replaces this field with selected fields)', 'secure-custom-fields' ),
711 ),
712 )
713 );
714
715 // layout
716 acf_render_field_setting(
717 $field,
718 array(
719 'label' => __( 'Layout', 'secure-custom-fields' ),
720 'instructions' => __( 'Specify the style used to render the selected fields', 'secure-custom-fields' ),
721 'type' => 'radio',
722 'name' => 'layout',
723 'layout' => 'horizontal',
724 'choices' => array(
725 'block' => __( 'Block', 'secure-custom-fields' ),
726 'table' => __( 'Table', 'secure-custom-fields' ),
727 'row' => __( 'Row', 'secure-custom-fields' ),
728 ),
729 )
730 );
731
732 // prefix_label
733 /* translators: %s: field label */
734 $instructions = __( 'Labels will be displayed as %s', 'secure-custom-fields' );
735 $instructions = sprintf( $instructions, '<code class="prefix-label-code-1"></code>' );
736 acf_render_field_setting(
737 $field,
738 array(
739 'label' => __( 'Prefix Field Labels', 'secure-custom-fields' ),
740 'instructions' => $instructions,
741 'name' => 'prefix_label',
742 'class' => 'setting-prefix-label',
743 'type' => 'true_false',
744 'ui' => 1,
745 )
746 );
747
748 // prefix_name
749 /* translators: %s: field name */
750 $instructions = __( 'Values will be saved as %s', 'secure-custom-fields' );
751 $instructions = sprintf( $instructions, '<code class="prefix-name-code-1"></code>' );
752 acf_render_field_setting(
753 $field,
754 array(
755 'label' => __( 'Prefix Field Names', 'secure-custom-fields' ),
756 'instructions' => $instructions,
757 'name' => 'prefix_name',
758 'class' => 'setting-prefix-name',
759 'type' => 'true_false',
760 'ui' => 1,
761 )
762 );
763 }
764
765
766 /**
767 * Returns an array of field choices for Select2.
768 *
769 * @param mixed $value The field value.
770 * @return array Array of choices for Select2.
771 */
772 public function get_clone_setting_choices( $value ) {
773
774 // vars
775 $choices = array();
776
777 // bail early if no $value
778 if ( empty( $value ) ) {
779 return $choices;
780 }
781
782 // force value to array
783 $value = acf_get_array( $value );
784
785 // loop
786 foreach ( $value as $v ) {
787 $choices[ $v ] = $this->get_clone_setting_choice( $v );
788 }
789
790 // return
791 return $choices;
792 }
793
794
795 /**
796 * Returns the label for a given clone choice.
797 *
798 * @param mixed $selector The field selector.
799 * @return string The choice label.
800 */
801 public function get_clone_setting_choice( $selector = '' ) {
802
803 // bail early no selector
804 if ( ! $selector ) {
805 return '';
806 }
807
808 // phpcs:disable WordPress.Security.NonceVerification.Missing -- Verified elsewhere.
809 // ajax_fields
810 if ( isset( $_POST['fields'][ $selector ] ) ) {
811 return $this->get_clone_setting_field_choice( acf_sanitize_request_args( wp_unslash( $_POST['fields'][ $selector ] ) ) );
812 }
813 // phpcs:enable WordPress.Security.NonceVerification.Missing
814
815 // field
816 if ( acf_is_field_key( $selector ) ) {
817 return $this->get_clone_setting_field_choice( acf_get_field( $selector ) );
818 }
819
820 // group
821 if ( acf_is_field_group_key( $selector ) ) {
822 return $this->get_clone_setting_group_choice( acf_get_field_group( $selector ) );
823 }
824
825 // return
826 return $selector;
827 }
828
829
830 /**
831 * Returns the text label for a field choice.
832 *
833 * @param array|false $field The field array.
834 * @return string The formatted choice text.
835 */
836 public function get_clone_setting_field_choice( $field ) {
837
838 // bail early if no field
839 if ( ! $field ) {
840 return __( 'Unknown field', 'secure-custom-fields' );
841 }
842
843 // title
844 $title = $field['label'] ? $field['label'] : __( '(no title)', 'secure-custom-fields' );
845
846 // append type
847 $title .= ' (' . $field['type'] . ')';
848
849 // ancestors
850 // - allow for AJAX to send through ancestors count
851 $ancestors = isset( $field['ancestors'] ) ? $field['ancestors'] : count( acf_get_field_ancestors( $field ) );
852 $title = str_repeat( '- ', $ancestors ) . $title;
853
854 // return
855 return $title;
856 }
857
858
859 /**
860 * Returns the text label for a field group choice.
861 *
862 * @param array|false $field_group The field group array.
863 * @return string The formatted choice text.
864 */
865 public function get_clone_setting_group_choice( $field_group ) {
866
867 // bail early if no field group
868 if ( ! $field_group ) {
869 return __( 'Unknown field group', 'secure-custom-fields' );
870 }
871
872 // return
873 /* translators: %s: field group title */
874 return sprintf( __( 'All fields from %s field group', 'secure-custom-fields' ), $field_group['title'] );
875 }
876
877
878 /**
879 * AJAX handler for getting potential fields to clone.
880 *
881 * @since ACF 5.3.8.3.8
882 *
883 * @return void
884 */
885 public function ajax_query() {
886 $nonce = acf_request_arg( 'nonce', '' );
887
888 if ( ! acf_verify_ajax( $nonce, 'acf/fields/clone/query' ) ) {
889 die();
890 }
891
892 // disable field to allow clone fields to appear selectable
893 acf_disable_filter( 'clone' );
894
895 // options
896 $options = acf_parse_args(
897 $_POST,
898 array(
899 'post_id' => 0,
900 'paged' => 0,
901 's' => '',
902 'title' => '',
903 'fields' => array(),
904 )
905 );
906
907 // vars
908 $results = array();
909 $s = false;
910 $i = -1;
911 $limit = 20;
912 $range_start = $limit * ( $options['paged'] - 1 ); // 0, 20, 40
913 $range_end = $range_start + ( $limit - 1 ); // 19, 39, 59
914
915 // search
916 if ( '' !== $options['s'] ) {
917
918 // strip slashes (search may be integer)
919 $s = wp_unslash( strval( $options['s'] ) );
920 }
921
922 // load groups
923 $field_groups = acf_get_field_groups();
924 $field_group = false;
925
926 // bail early if no field groups
927 if ( empty( $field_groups ) ) {
928 die();
929 }
930
931 // move current field group to start
932 foreach ( array_keys( $field_groups ) as $j ) {
933
934 // check ID
935 if ( $field_groups[ $j ]['ID'] !== $options['post_id'] ) {
936 continue;
937 }
938
939 // extract field group and move to start
940 $field_group = acf_extract_var( $field_groups, $j );
941
942 // field group found, stop looking
943 break;
944 }
945
946 // if field group was not found, this is a new field group (not yet saved)
947 if ( ! $field_group ) {
948 $field_group = array(
949 'ID' => $options['post_id'],
950 'title' => $options['title'],
951 'key' => '',
952 );
953 }
954
955 // move current field group to start of list
956 array_unshift( $field_groups, $field_group );
957
958 // loop
959 foreach ( $field_groups as $field_group ) {
960
961 // vars
962 $fields = false;
963 $ignore_s = false;
964 $data = array(
965 'text' => $field_group['title'],
966 'children' => array(),
967 );
968
969 // get fields
970 if ( (int) $field_group['ID'] === (int) $options['post_id'] ) {
971 $fields = $options['fields'];
972 } else {
973 $fields = acf_get_fields( $field_group );
974 $fields = acf_prepare_fields_for_import( $fields );
975 }
976
977 // bail early if no fields
978 if ( ! $fields ) {
979 continue;
980 }
981
982 // show all children for field group search match
983 if ( false !== $s && stripos( $data['text'], $s ) !== false ) {
984 $ignore_s = true;
985 }
986
987 // populate children
988 $children = array();
989 $children[] = $field_group['key'];
990 foreach ( $fields as $field ) {
991 $children[] = $field['key'];
992 }
993
994 // loop
995 foreach ( $children as $child ) {
996
997 // bail early if no key (fake field group or corrupt field)
998 if ( ! $child ) {
999 continue;
1000 }
1001
1002 // vars
1003 $text = false;
1004
1005 // bail early if is search, and $text does not contain $s
1006 if ( false !== $s && ! $ignore_s ) {
1007
1008 // get early
1009 $text = $this->get_clone_setting_choice( $child );
1010
1011 // search
1012 if ( stripos( $text, $s ) === false ) {
1013 continue;
1014 }
1015 }
1016
1017 // $i
1018 ++$i;
1019
1020 // bail early if $i is out of bounds
1021 if ( $i < $range_start || $i > $range_end ) {
1022 continue;
1023 }
1024
1025 // load text
1026 if ( false === $text ) {
1027 $text = $this->get_clone_setting_choice( $child );
1028 }
1029
1030 // append
1031 $data['children'][] = array(
1032 'id' => $child,
1033 'text' => $text,
1034 );
1035 }
1036
1037 // bail early if no children
1038 // - this group contained fields, but none shown on this page
1039 if ( empty( $data['children'] ) ) {
1040 continue;
1041 }
1042
1043 // append
1044 $results[] = $data;
1045
1046 // end loop if $i is out of bounds
1047 // - no need to look further
1048 if ( $i > $range_end ) {
1049 break;
1050 }
1051 }
1052
1053 // return
1054 acf_send_ajax_results(
1055 array(
1056 'results' => $results,
1057 'limit' => $limit,
1058 )
1059 );
1060 }
1061
1062
1063 /**
1064 * Restores a field's key ready for input.
1065 *
1066 * @since ACF 5.4.0
1067 *
1068 * @param array $field The field array.
1069 * @return array The modified field array.
1070 */
1071 public function acf_prepare_field( $field ) {
1072
1073 // bail early if not cloned
1074 if ( empty( $field['_clone'] ) ) {
1075 return $field;
1076 }
1077
1078 // restore key
1079 if ( isset( $field['__key'] ) ) {
1080 $field['key'] = $field['__key'];
1081 }
1082
1083 // return
1084 return $field;
1085 }
1086
1087
1088 /**
1089 * Validates the value of a clone field.
1090 *
1091 * @since ACF 5.0.0
1092 *
1093 * @param bool $valid Whether the value is valid.
1094 * @param mixed $value The field value.
1095 * @param array $field The field array.
1096 * @param string $input The input element's name attribute.
1097 * @return bool Whether the value is valid.
1098 */
1099 public function validate_value( $valid, $value, $field, $input ) {
1100
1101 // bail early if no $value
1102 if ( empty( $value ) ) {
1103 return $valid;
1104 }
1105
1106 // bail early if no sub fields
1107 if ( empty( $field['sub_fields'] ) ) {
1108 return $valid;
1109 }
1110
1111 // loop
1112 foreach ( array_keys( $field['sub_fields'] ) as $i ) {
1113
1114 // get sub field
1115 $sub_field = $field['sub_fields'][ $i ];
1116 $k = $sub_field['key'];
1117
1118 // bail early if value not set (conditional logic?)
1119 if ( ! isset( $value[ $k ] ) ) {
1120 continue;
1121 }
1122
1123 // validate
1124 acf_validate_value( $value[ $k ], $sub_field, "{$input}[{$k}]" );
1125 }
1126
1127 // return
1128 return $valid;
1129 }
1130
1131 /**
1132 * Returns the schema array for the REST API.
1133 *
1134 * @param array $field The field array.
1135 * @return array The schema array for the REST API.
1136 */
1137 public function get_rest_schema( array $field ) {
1138 $schema = array(
1139 'type' => array( 'object', 'null' ),
1140 'required' => ! empty( $field['required'] ) ? array() : false,
1141 'items' => array(
1142 'type' => 'object',
1143 'properties' => array(),
1144 ),
1145 );
1146
1147 foreach ( $field['sub_fields'] as $sub_field ) {
1148 /**
1149 * Field type instance.
1150 *
1151 * @var acf_field $type
1152 */
1153 $type = acf_get_field_type( $sub_field['type'] );
1154
1155 if ( ! $type ) {
1156 continue;
1157 }
1158
1159 $sub_field_schema = $type->get_rest_schema( $sub_field );
1160
1161 // Passing null to nested fields has no effect. Remove this as a possible type to prevent
1162 // confusion in the schema.
1163 $null_type_index = array_search( 'null', $sub_field_schema['type'], true );
1164 if ( false !== $null_type_index ) {
1165 unset( $sub_field_schema['type'][ $null_type_index ] );
1166 }
1167
1168 $schema['items']['properties'][ $sub_field['name'] ] = $sub_field_schema;
1169
1170 /**
1171 * If the clone field itself is marked as required, all subfields are required,
1172 * regardless of the status of the original fields.
1173 */
1174 if ( is_array( $schema['required'] ) ) {
1175 $schema['required'][] = $sub_field['name'];
1176 }
1177 }
1178
1179 return $schema;
1180 }
1181 }
1182
1183
1184 // initialize
1185 acf_register_field_type( 'acf_field_clone' );
1186 endif; // class_exists check
1187
1188 ?>