PluginProbe ʕ •ᴥ•ʔ
Secure Custom Fields / 6.9.1
Secure Custom Fields v6.9.1
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 / post-types / class-acf-field-group.php
secure-custom-fields / includes / post-types Last commit date
class-acf-field-group.php 2 months ago class-acf-post-type.php 2 months ago class-acf-taxonomy.php 2 months ago class-acf-ui-options-page.php 1 year ago class-scf-field-manager.php 6 months ago index.php 1 year ago
class-acf-field-group.php
580 lines
1 <?php
2
3 if ( ! defined( 'ABSPATH' ) ) {
4 exit; // Exit if accessed directly.
5 }
6
7 if ( ! class_exists( 'ACF_Field_Group' ) ) {
8 class ACF_Field_Group extends ACF_Internal_Post_Type {
9
10 /**
11 * The ACF internal post type name.
12 *
13 * @var string
14 */
15 public $post_type = 'acf-field-group';
16
17 /**
18 * The prefix for the key used in the main post array.
19 *
20 * @var string
21 */
22 public $post_key_prefix = 'group_';
23
24 /**
25 * The cache key for a singular post.
26 *
27 * @var string
28 */
29 public $cache_key = 'acf_get_field_group_post:key:';
30
31 /**
32 * The cache key for a collection of posts.
33 *
34 * @var string
35 */
36 public $cache_key_plural = 'acf_get_field_group_posts';
37
38 /**
39 * The hook name for a singular post.
40 *
41 * @var string
42 */
43 public $hook_name = 'field_group';
44
45 /**
46 * The hook name for a collection of posts.
47 *
48 * @var string
49 */
50 public $hook_name_plural = 'field_groups';
51
52 /**
53 * The name of the store used for the post type.
54 *
55 * @var string
56 */
57 public $store = 'field-groups';
58
59 /**
60 * Constructs the class.
61 */
62 public function __construct() {
63 // Include admin classes in admin.
64 if ( is_admin() ) {
65 acf_include( 'includes/admin/admin-internal-post-type-list.php' );
66 acf_include( 'includes/admin/admin-internal-post-type.php' );
67 acf_include( 'includes/admin/post-types/admin-field-group.php' );
68 acf_include( 'includes/admin/post-types/admin-field-groups.php' );
69 }
70
71 parent::__construct();
72 add_filter( 'acf/pre_update_field_group', array( $this, 'pre_update_field_group' ), 1 );
73 }
74
75 /**
76 * Gets the default settings array for an ACF field group.
77 *
78 * @return array
79 */
80 public function get_settings_array() {
81 return array(
82 'ID' => 0,
83 'key' => '',
84 'title' => '',
85 'fields' => array(),
86 'location' => array(),
87 'menu_order' => 0,
88 'position' => 'normal',
89 'style' => 'default',
90 'label_placement' => 'top',
91 'instruction_placement' => 'label',
92 'hide_on_screen' => array(),
93 'active' => true,
94 'description' => '',
95 'show_in_rest' => false,
96 'display_title' => '',
97 'allow_ai_access' => false,
98 'ai_description' => '',
99 );
100 }
101
102 /**
103 * Get an ACF CPT object as an array.
104 *
105 * @since ACF 6.1
106 *
107 * @param integer|WP_Post $id The post ID being queried.
108 * @return array|boolean The main ACF array for the post, or false on failure.
109 */
110 public function get_post( $id = 0 ) {
111 // Allow WP_Post to be passed.
112 if ( is_object( $id ) ) {
113 $id = $id->ID;
114 }
115
116 // Check store.
117 $store = acf_get_store( $this->store );
118 if ( $store->has( $id ) ) {
119 return $store->get( $id );
120 }
121
122 if ( acf_is_local_field_group( $id ) ) {
123 $post = acf_get_local_field_group( $id );
124 } else {
125 $post = $this->get_raw_post( $id );
126 }
127
128 // Bail early if no post.
129 if ( ! $post ) {
130 return false;
131 }
132
133 $post = $this->validate_post( $post );
134
135 /**
136 * Filters the post array after it has been loaded.
137 *
138 * @date 12/02/2014
139 * @since ACF 5.0.0
140 *
141 * @param array $post The post array.
142 */
143 $post = apply_filters( "acf/load_{$this->hook_name}", $post );
144
145 // Store field group using aliases to also find via key, ID and name.
146 $store->set( $post['key'], $post );
147 $store->alias( $post['key'], $post['ID'] );
148
149 return $post;
150 }
151
152 /**
153 * Filter the posts returned by $this->get_posts().
154 *
155 * By default, field groups are filtered based on their location rules. This
156 * determines which field groups appear on specific edit screens (e.g., a
157 * "Product" field group only shows when editing WooCommerce products).
158 *
159 * IMPORTANT: Location rule filtering is a UX feature, not a security mechanism.
160 * It controls which field groups are contextually relevant to display, not
161 * who can access them. Access control is handled by WordPress capabilities.
162 *
163 * Pass 'ignore_location_rules' => true to bypass location-based filtering and
164 * return all field groups. Use this for admin listings, REST/abilities APIs,
165 * and other programmatic contexts where all field groups should be listed
166 * regardless of their location rules.
167 *
168 * @since ACF 6.1
169 *
170 * @param array $posts An array of posts to filter.
171 * @param array $args An array of args to filter by.
172 * - 'ignore_location_rules': Bypass location filtering when true.
173 * - 'active': Filter by active status (handled by parent).
174 * @return array
175 */
176 public function filter_posts( $posts, $args = array() ) {
177 // If ignore_location_rules is set, bypass location-based filtering
178 // and use parent's filter (which only filters by active status).
179 if ( ! empty( $args['ignore_location_rules'] ) ) {
180 return parent::filter_posts( $posts, $args );
181 }
182
183 // Loop over field groups and check visibility.
184 $filtered = array();
185 if ( $posts ) {
186 foreach ( $posts as $post ) {
187 if ( acf_get_field_group_visibility( $post, $args ) ) {
188 $filtered[] = $post;
189 }
190 }
191 }
192
193 return $filtered;
194 }
195
196 /**
197 * Filters the field group data before it is updated in the database.
198 *
199 * @since ACF 6.1
200 *
201 * @param array $field_group The field group being updated.
202 * @return array
203 */
204 public function pre_update_field_group( $field_group ) {
205 // Remove empty values and convert to associated array.
206 if ( $field_group['location'] ) {
207 $field_group['location'] = array_filter( $field_group['location'] );
208 $field_group['location'] = array_values( $field_group['location'] );
209 $field_group['location'] = array_map( 'array_filter', $field_group['location'] );
210 $field_group['location'] = array_map( 'array_values', $field_group['location'] );
211 }
212
213 return $field_group;
214 }
215
216 /**
217 * Deletes an ACF field group and related fields.
218 *
219 * @since ACF 6.1
220 *
221 * @param integer|string $id The ID of the field group to delete.
222 * @return boolean
223 */
224 public function delete_post( $id = 0 ) {
225 // Disable filters to ensure ACF loads data from DB.
226 acf_disable_filters();
227
228 // Get the post.
229 $post = $this->get_post( $id );
230
231 // Bail early if post was not found.
232 if ( ! $post || ! $post['ID'] ) {
233 return false;
234 }
235
236 // Delete the fields.
237 $fields = acf_get_fields( $post );
238 if ( $fields ) {
239 foreach ( $fields as $field ) {
240 acf_delete_field( $field['ID'] );
241 }
242 }
243
244 // Delete post and flush cache.
245 wp_delete_post( $post['ID'], true );
246 $this->flush_post_cache( $post );
247
248 /**
249 * Fires immediately after an ACF post has been deleted.
250 *
251 * @date 12/02/2014
252 * @since ACF 5.0.0
253 *
254 * @param array $post The ACF post array.
255 */
256 do_action( "acf/delete_{$this->hook_name}", $post );
257
258 return true;
259 }
260
261 /**
262 * Trashes an ACF field group and related fields.
263 *
264 * @since ACF 6.1
265 *
266 * @param integer|string $id The ID of the field group to trash.
267 * @return boolean
268 */
269 public function trash_post( $id = 0 ) {
270 // Disable filters to ensure ACF loads data from DB.
271 acf_disable_filters();
272
273 $post = $this->get_post( $id );
274 if ( ! $post || ! $post['ID'] ) {
275 return false;
276 }
277
278 // Trash fields.
279 $fields = acf_get_fields( $post );
280 if ( $fields ) {
281 foreach ( $fields as $field ) {
282 acf_trash_field( $field['ID'] );
283 }
284 }
285
286 wp_trash_post( $post['ID'] );
287 $this->flush_post_cache( $post );
288
289 /**
290 * Fires immediately after a field_group has been trashed.
291 *
292 * @date 12/02/2014
293 * @since ACF 5.0.0
294 *
295 * @param array $post The ACF post array.
296 */
297 do_action( "acf/trash_{$this->hook_name}", $post );
298
299 return true;
300 }
301
302 /**
303 * Restores an ACF field group and related fields from the trash.
304 *
305 * @since ACF 6.1
306 *
307 * @param integer|string $id The ID of the ACF post to untrash.
308 * @return boolean
309 */
310 public function untrash_post( $id = 0 ) {
311 // Disable filters to ensure ACF loads data from DB.
312 acf_disable_filters();
313
314 $post = $this->get_post( $id );
315 if ( ! $post || ! $post['ID'] ) {
316 return false;
317 }
318
319 $fields = acf_get_fields( $post );
320 if ( $fields ) {
321 foreach ( $fields as $field ) {
322 acf_untrash_field( $field['ID'] );
323 }
324 }
325
326 wp_untrash_post( $post['ID'] );
327 $this->flush_post_cache( $post );
328
329 /**
330 * Fires immediately after an ACF post has been untrashed.
331 *
332 * @date 12/02/2014
333 * @since ACF 5.0.0
334 *
335 * @param array $post The ACF post array.
336 */
337 do_action( "acf/untrash_{$this->hook_name}", $post );
338
339 return true;
340 }
341
342 /**
343 * Duplicates an ACF post.
344 *
345 * @since ACF 6.1
346 *
347 * @param integer|string $id The ID of the post to duplicate.
348 * @param integer $new_post_id Optional post ID to override.
349 * @return array The new ACF post array.
350 */
351 public function duplicate_post( $id = 0, $new_post_id = 0 ) {
352 // Disable filters to ensure ACF loads data from DB.
353 acf_disable_filters();
354
355 $post = $this->get_post( $id );
356 if ( ! $post || ! $post['ID'] ) {
357 return false;
358 }
359
360 // Get fields before updating field group attributes.
361 $fields = acf_get_fields( $post['ID'] );
362
363 // Update attributes.
364 $post['ID'] = $new_post_id;
365 $post['key'] = uniqid( 'group_' );
366
367 // Add (copy) to title when appropriate.
368 if ( ! $new_post_id ) {
369 $post['title'] .= ' (' . __( 'copy', 'secure-custom-fields' ) . ')';
370 }
371
372 // When duplicating a field group, insert a temporary post and set the field group's ID.
373 // This allows fields to be updated before the field group (field group ID is needed for field parent setting).
374 if ( ! $post['ID'] ) {
375 $post['ID'] = wp_insert_post(
376 array(
377 'post_title' => $post['key'],
378 'post_type' => $this->post_type,
379 )
380 );
381 }
382
383 // Duplicate fields and update post.
384 acf_duplicate_fields( $fields, $post['ID'] );
385 $post = $this->update_post( $post );
386
387 /**
388 * Fires immediately after an ACF post has been duplicated.
389 *
390 * @date 12/02/2014
391 * @since ACF 5.0.0
392 *
393 * @param array $post The ACF post array.
394 */
395 do_action( "acf/duplicate_{$this->hook_name}", $post );
396
397 return $post;
398 }
399
400 /**
401 * Returns a modified ACF field group array ready for export.
402 *
403 * @since ACF 6.1
404 *
405 * @param array $post The ACF post array.
406 * @return array
407 */
408 public function prepare_post_for_export( $post = array() ) {
409 // Remove args.
410 acf_extract_vars( $post, array( 'ID', 'local', '_valid' ) );
411
412 // Prepare fields.
413 $post['fields'] = acf_prepare_fields_for_export( $post['fields'] );
414
415 /**
416 * Filters the ACF post array before being returned to the export tool.
417 *
418 * @date 12/02/2014
419 * @since ACF 5.0.0
420 *
421 * @param array $post The ACF post array.
422 */
423 return apply_filters( "acf/prepare_{$this->hook_name}_for_export", $post );
424 }
425
426 /**
427 * Prepares an ACF field group for import.
428 *
429 * @since ACF 6.1
430 *
431 * @param array $post The ACF field group array.
432 * @return array
433 */
434 public function prepare_post_for_import( $post ) {
435 // Update parent and menu_order properties for all fields.
436 if ( ! empty( $post['fields'] ) ) {
437 foreach ( $post['fields'] as $i => &$field ) {
438 $field['parent'] = $post['key'];
439 $field['menu_order'] = $i;
440 }
441 }
442
443 /**
444 * Filters the ACF post array before being returned to the import tool.
445 *
446 * @date 21/11/19
447 * @since ACF 5.8.8
448 *
449 * @param array $post The ACF post array.
450 */
451 return apply_filters( "acf/prepare_{$this->hook_name}_for_import", $post );
452 }
453
454 /**
455 * Returns a string that can be used to create a field group with PHP.
456 *
457 * @since ACF 6.1
458 *
459 * @param array $post The main field group array.
460 * @return string
461 */
462 public function export_post_as_php( $post = array() ) {
463 $return = '';
464 if ( empty( $post ) ) {
465 return $return;
466 }
467
468 $code = var_export( $post, true ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions -- Used for PHP export.
469 if ( ! $code ) {
470 return $return;
471 }
472
473 $code = $this->format_code_for_export( $code );
474 $return .= "acf_add_local_field_group( {$code} );\r\n";
475
476 return esc_textarea( $return );
477 }
478
479 /**
480 * Imports an ACF post into the database.
481 *
482 * @since ACF 6.1
483 *
484 * @param array $post The ACF post array.
485 * @return array
486 */
487 public function import_post( $post ) {
488 // Disable filters to ensure data is not modified by local, clone, etc.
489 $filters = acf_disable_filters();
490
491 // Validate the post (ensures all settings exist).
492 $post = $this->get_valid_post( $post );
493
494 // Prepare post for import (modifies settings).
495 $post = $this->prepare_post_for_import( $post );
496
497 // Prepare fields for import (modifies settings).
498 $fields = acf_prepare_fields_for_import( $post['fields'] );
499
500 // Stores a map of field "key" => "ID".
501 $ids = array();
502
503 // If the field group has an ID, review and delete stale fields in the database.
504 if ( $post['ID'] ) {
505
506 // Load database fields.
507 $db_fields = acf_prepare_fields_for_import( acf_get_fields( $post ) );
508
509 // Generate map of "index" => "key" data.
510 $keys = wp_list_pluck( $fields, 'key' );
511
512 // Loop over db fields and delete those who don't exist in $new_fields.
513 foreach ( $db_fields as $field ) {
514 // Add field data to $ids map.
515 $ids[ $field['key'] ] = $field['ID'];
516
517 // Delete field if not in $keys.
518 if ( ! in_array( $field['key'], $keys, true ) ) {
519 acf_delete_field( $field['ID'] );
520 }
521 }
522 }
523
524 // When importing a new field group, insert a temporary post and set the field group's ID.
525 // This allows fields to be updated before the field group (field group ID is needed for field parent setting).
526 if ( ! $post['ID'] ) {
527 $post['ID'] = wp_insert_post(
528 array(
529 'post_title' => $post['key'],
530 'post_type' => $this->post_type,
531 )
532 );
533 }
534 // Add field group data to $ids map.
535 $ids[ $post['key'] ] = $post['ID'];
536
537 // Loop over and add fields.
538 if ( $fields ) {
539 foreach ( $fields as $field ) {
540
541 // Replace any "key" references with "ID".
542 if ( isset( $ids[ $field['key'] ] ) ) {
543 $field['ID'] = $ids[ $field['key'] ];
544 }
545 if ( isset( $ids[ $field['parent'] ] ) ) {
546 $field['parent'] = $ids[ $field['parent'] ];
547 }
548
549 // Save field.
550 $field = acf_update_field( $field );
551
552 // Add field data to $ids map for children.
553 $ids[ $field['key'] ] = $field['ID'];
554 }
555 }
556
557 // Save field group.
558 $post = $this->update_post( $post );
559
560 // Enable filters again.
561 acf_enable_filters( $filters );
562
563 /**
564 * Fires immediately after an ACF post has been imported.
565 *
566 * @date 12/02/2014
567 * @since ACF 5.0.0
568 *
569 * @param array $post The ACF post array.
570 */
571 do_action( "acf/import_{$this->hook_name}", $post );
572
573 return $post;
574 }
575 }
576
577 }
578
579 acf_new_instance( 'ACF_Field_Group' );
580