PluginProbe ʕ •ᴥ•ʔ
Advanced Custom Fields (ACF®) / 6.8.7
Advanced Custom Fields (ACF®) v6.8.7
6.8.9 6.8.8 6.8.7 6.8.6 6.8.5 6.8.4 6.8.3 6.8.2 6.8.1 5.8.5 5.8.6 5.8.7 5.8.8 5.8.9 5.9.0 5.9.1 5.9.2 5.9.3 5.9.4 5.9.5 5.9.6 5.9.7 5.9.8 5.9.9 6.0.0 6.0.1 6.0.2 6.0.3 6.0.4 6.0.5 6.0.6 6.0.7 6.1.0 6.1.1 6.1.2 6.1.3 6.1.4 6.1.5 6.1.6 6.1.7 6.1.8 6.2.0 6.2.1 6.2.2 6.2.3 6.2.4 6.2.5 6.2.6 6.2.6.1 6.2.7 6.2.8 6.2.9 6.3.0 6.3.1 6.3.10.2 6.3.11 6.3.12 6.3.2 6.3.3 6.3.4 6.3.5 6.3.6 6.3.6.1 6.4.0 6.4.0.1 6.4.1 6.4.2 6.4.3 6.5.0 6.5.1 6.6.0 6.6.1 6.6.2 6.7.0 6.7.1 6.7.2 6.8.0 trunk 1.0.0 1.0.2 1.0.3 1.0.5 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4 2.0.5 2.1.1 2.1.3 2.1.4 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.0.6 3.0.7 3.1.0 3.1.1 3.1.2 3.1.3 3.1.4 3.1.5 3.1.6 3.1.7 3.1.8 3.1.9 3.2.0 3.2.2 3.2.3 3.2.4 3.2.5 3.2.6 3.2.7 3.2.8 3.2.9 3.3.0 3.3.1 3.3.2 3.3.3 3.3.4 3.3.5 3.3.6 3.3.7 3.3.8 3.3.9 3.4.0 3.4.1 3.4.2 3.4.3 3.5.0 3.5.1 3.5.2 3.5.3 3.5.4 3.5.5 3.5.6 3.5.7 3.5.8 4.0.0 4.0.1 4.0.2 4.0.3 4.1.0 4.1.1 4.1.2 4.1.3 4.1.4 4.1.5 4.1.6 4.1.8 4.2.0 4.2.1 4.2.2 4.3.0 4.3.1 4.3.2 4.3.3 4.3.4 4.3.5 4.3.6 4.3.7 4.3.8 4.3.9 4.4.0 4.4.1 4.4.10 4.4.11 4.4.12 4.4.2 4.4.3 4.4.4 4.4.5 4.4.6 4.4.7 4.4.8 4.4.9 5.10 5.10.1 5.10.2 5.11 5.11.1 5.11.2 5.11.3 5.11.4 5.12 5.12.1 5.12.2 5.12.3 5.12.4 5.12.5 5.12.6 5.6.10 5.6.2 5.6.3 5.6.4 5.6.5 5.6.6 5.6.7 5.6.8 5.6.9 5.7.0 5.7.1 5.7.10 5.7.12 5.7.13 5.7.2 5.7.3 5.7.4 5.7.5 5.7.6 5.7.7 5.7.8 5.7.9 5.8.0 5.8.1 5.8.10 5.8.11 5.8.12 5.8.13 5.8.14 5.8.2 5.8.3 5.8.4
advanced-custom-fields / src / AI / Abilities / FieldGroup.php
advanced-custom-fields / src / AI / Abilities Last commit date
ACF_REST_Ability.php 5 months ago Abilities.php 5 months ago AbstractAbilityGroup.php 5 months ago FieldGroup.php 3 months ago PostType.php 5 months ago Taxonomy.php 3 months ago
FieldGroup.php
583 lines
1 <?php
2 /**
3 * @package ACF
4 * @author WP Engine
5 *
6 * © 2026 Advanced Custom Fields (ACF®). All rights reserved.
7 * "ACF" is a trademark of WP Engine.
8 * Licensed under the GNU General Public License v2 or later.
9 * https://www.gnu.org/licenses/gpl-2.0.html
10 */
11
12 namespace ACF\AI\Abilities;
13
14 use WP_Error;
15
16 // Exit if accessed directly.
17 defined( 'ABSPATH' ) || exit;
18
19 /**
20 * ACF Field Group Abilities
21 *
22 * Handles ACF field group related abilities for the WordPress Abilities API.
23 */
24 class FieldGroup extends AbstractAbilityGroup {
25
26 /**
27 * Register field group related abilities.
28 *
29 * @since 6.8.0
30 *
31 * @return void
32 */
33 public function register_abilities() {
34 if ( ! $this->is_abilities_api_available() ) {
35 return;
36 }
37
38 // Register ACF field groups ability.
39 $this->register_ability(
40 'acf/field-groups',
41 array(
42 'label' => __( 'List ACF Field Groups', 'acf' ),
43 'description' => __( 'Get all ACF field groups that allow AI access.', 'acf' ),
44 'category' => 'acf-field-management',
45 'input_schema' => array(
46 'type' => array( 'object', 'null' ),
47 'properties' => array(),
48 'additionalProperties' => false,
49 ),
50 'output_schema' => array(
51 'type' => 'object',
52 'properties' => array(
53 'field_groups' => array(
54 'type' => 'array',
55 'items' => array(
56 'type' => 'object',
57 ),
58 ),
59 'count' => array(
60 'type' => 'integer',
61 ),
62 'message' => array(
63 'type' => 'string',
64 ),
65 ),
66 ),
67 'execute_callback' => array( $this, 'get_field_groups' ),
68 'permission_callback' => function () {
69 return current_user_can( acf_get_setting( 'capability' ) );
70 },
71 'meta' => array(
72 'annotations' => array(
73 'readonly' => true,
74 'destructive' => false,
75 ),
76 'show_in_rest' => true,
77 ),
78 )
79 );
80
81 // Register field group ability.
82 $this->register_ability(
83 'acf/register-field-group',
84 array(
85 'label' => __( 'Register ACF Field Group', 'acf' ),
86 'description' => __( 'Register a new ACF field group schema with field definitions. This creates the field structure that will appear on content, not the field values themselves. Field values are set when creating or updating posts, terms, or other content.', 'acf' ),
87 'category' => 'acf-field-management',
88 'input_schema' => array(
89 'type' => 'object',
90 'properties' => array(
91 'title' => array(
92 'type' => 'string',
93 'description' => 'The title of the field group',
94 'minLength' => 1,
95 ),
96 'fields' => $this->get_fields_schema(),
97 'location' => $this->get_location_schema(),
98 'description' => array(
99 'type' => 'string',
100 'description' => 'A description for this field group',
101 ),
102 'position' => array(
103 'type' => 'string',
104 'description' => 'Where to show the field group (normal, side, acf_after_title)',
105 'enum' => array( 'normal', 'side', 'acf_after_title' ),
106 'default' => 'normal',
107 ),
108 'style' => array(
109 'type' => 'string',
110 'description' => 'Field group style (default, seamless)',
111 'enum' => array( 'default', 'seamless' ),
112 'default' => 'default',
113 ),
114 'label_placement' => array(
115 'type' => 'string',
116 'description' => 'Where to place field labels (top, left)',
117 'enum' => array( 'top', 'left' ),
118 'default' => 'top',
119 ),
120 'instruction_placement' => array(
121 'type' => 'string',
122 'description' => 'Where to show field instructions (label, field)',
123 'enum' => array( 'label', 'field' ),
124 'default' => 'label',
125 ),
126 'hide_on_screen' => array(
127 'type' => 'array',
128 'description' => 'Items that should be hidden from the edit screen containing this field group',
129 'items' => array(
130 'type' => 'string',
131 'enum' => array(
132 'permalink',
133 'the_content',
134 'excerpt',
135 'custom_fields',
136 'discussion',
137 'comments',
138 'revisions',
139 'slug',
140 'author',
141 'format',
142 'page_attributes',
143 'featured_image',
144 'categories',
145 'tags',
146 'send-trackbacks',
147 ),
148 ),
149 ),
150 'active' => array(
151 'type' => 'boolean',
152 'description' => 'Whether the field group is active',
153 'default' => true,
154 ),
155 'show_in_rest' => array(
156 'type' => 'boolean',
157 'description' => 'Whether the field group is shown in the REST API',
158 'default' => true,
159 ),
160 'allow_ai_access' => array(
161 'type' => 'boolean',
162 'description' => 'Whether the field group allows access to AI',
163 'default' => true,
164 ),
165 'ai_description' => array(
166 'type' => 'string',
167 'description' => 'A short description of the field group to provide AI more context',
168 ),
169 ),
170 'required' => array( 'title', 'fields', 'location' ),
171 'additionalProperties' => false,
172 ),
173 'output_schema' => array(
174 'type' => 'object',
175 'properties' => array(
176 'success' => array(
177 'type' => 'boolean',
178 ),
179 'field_group' => array(
180 'type' => 'object',
181 'properties' => array(
182 'ID' => array( 'type' => 'integer' ),
183 'key' => array( 'type' => 'string' ),
184 'title' => array( 'type' => 'string' ),
185 'fields' => array( 'type' => 'array' ),
186 'location' => array( 'type' => 'array' ),
187 'position' => array( 'type' => 'string' ),
188 'style' => array( 'type' => 'string' ),
189 'label_placement' => array( 'type' => 'string' ),
190 'instruction_placement' => array( 'type' => 'string' ),
191 'active' => array( 'type' => 'boolean' ),
192 'description' => array( 'type' => 'string' ),
193 'show_in_rest' => array( 'type' => 'boolean' ),
194 'allow_ai_access' => array( 'type' => 'boolean' ),
195 'ai_description' => array( 'type' => 'string' ),
196 ),
197 ),
198 'field_group_id' => array(
199 'type' => 'integer',
200 'description' => 'The ID of the created field group',
201 ),
202 'message' => array(
203 'type' => 'string',
204 ),
205 ),
206 ),
207 'execute_callback' => array( $this, 'create_field_group' ),
208 'permission_callback' => function () {
209 return current_user_can( acf_get_setting( 'capability' ) );
210 },
211 'meta' => array(
212 'annotations' => array(
213 'destructive' => false,
214 'idempotent' => true,
215 ),
216 'show_in_rest' => true,
217 ),
218 )
219 );
220 }
221
222 /**
223 * Get the field schema that includes all registered field types.
224 *
225 * Returns a JSON Schema with oneOf containing schemas for all ACF field types,
226 * allowing the AI to see available properties for each field type.
227 *
228 * @since 6.8.0
229 *
230 * @return array
231 */
232 private function get_fields_schema(): array {
233 $field_types = acf_get_field_types();
234 $schemas = array();
235
236 foreach ( $field_types as $field_type ) {
237 // Get the schema for this field type.
238 $schema = array();
239 if ( method_exists( $field_type, 'get_field_creation_schema' ) ) {
240 $schema = $field_type->get_field_creation_schema();
241 }
242
243 // Skip if the schema is empty.
244 if ( empty( $schema ) ) {
245 continue;
246 }
247
248 $schemas[] = $schema;
249 }
250
251 return array(
252 'type' => 'array',
253 'description' => 'Array of fields to add to the field group',
254 'minItems' => 1,
255 'items' => array(
256 'oneOf' => $schemas,
257 ),
258 );
259 }
260
261 /**
262 * Returns the schema needed to create field group location rules.
263 *
264 * @since 6.8.0
265 *
266 * @return array
267 */
268 private function get_location_schema(): array {
269 // Get all location types organized by category
270 $location_types = acf_get_location_rule_types();
271
272 // Build oneOf schemas for each location type
273 $location_rule_schemas = array();
274
275 foreach ( $location_types as $types ) {
276 foreach ( $types as $param => $label ) {
277 // Create a sample rule to get operators and values
278 $sample_rule = array( 'param' => $param );
279
280 // Get operators for this param
281 $operators = acf_get_location_rule_operators( $sample_rule );
282
283 // Build schema for this specific location type
284 $location_rule_schemas[] = array(
285 'type' => 'object',
286 'properties' => array(
287 'param' => array(
288 'type' => 'string',
289 'enum' => array( $param ),
290 'description' => $label,
291 ),
292 'operator' => array(
293 'type' => 'string',
294 'enum' => array_keys( $operators ),
295 'description' => 'Comparison operator',
296 'default' => '==',
297 ),
298 'value' => array(
299 'type' => 'string',
300 'description' => sprintf( 'Value for %s', $label ),
301 ),
302 ),
303 'required' => array( 'param', 'operator', 'value' ),
304 );
305 }
306 }
307
308 // Return full location schema supporting multiple groups and rules
309 return array(
310 'type' => 'array',
311 'description' => 'Location rules determining where this field group appears. Each array item is an OR group containing AND rules.',
312 'minItems' => 1,
313 'items' => array(
314 'type' => 'array',
315 'description' => 'Group of location rules (AND logic)',
316 'minItems' => 1,
317 'items' => array(
318 'oneOf' => $location_rule_schemas,
319 ),
320 ),
321 );
322 }
323
324 /**
325 * Callback for the "acf/get-field-groups" ability.
326 *
327 * @since 6.8.0
328 *
329 * @param array $input Ability input (unused).
330 * @return array
331 */
332 public function get_field_groups( $input = array() ) {
333 unset( $input ); // Not used, but required by interface.
334
335 $field_groups = $this->get_ai_accessible_field_groups();
336 $count = count( $field_groups );
337
338 return array(
339 'field_groups' => $field_groups,
340 'count' => $count,
341 'message' => sprintf(
342 /* translators: %d: Number of found field groups */
343 _n( 'Found %d ACF field group.', 'Found %d ACF field groups.', $count, 'acf' ),
344 $count
345 ),
346 );
347 }
348
349 /**
350 * A helper function to get the field groups that allow AI access.
351 *
352 * @since 6.8.0
353 *
354 * @return array
355 */
356 public function get_ai_accessible_field_groups(): array {
357 $field_groups = acf_get_field_groups();
358 $ai_accessible = array();
359
360 foreach ( $field_groups as $field_group ) {
361 if ( $this->is_field_group_ai_accessible( $field_group ) ) {
362 $ai_accessible[] = $field_group;
363 }
364 }
365
366 return $ai_accessible;
367 }
368
369 /**
370 * Check if a field group allows AI access.
371 *
372 * @since 6.8.0
373 *
374 * @param array $field_group Field group array.
375 * @return boolean
376 */
377 private function is_field_group_ai_accessible( $field_group ): bool {
378 return ! empty( $field_group['allow_ai_access'] );
379 }
380
381 /**
382 * Callback for the "acf/register-field-group" ability.
383 *
384 * @since 6.8.0
385 *
386 * @param array $input Ability arguments containing title and fields.
387 * @return array|WP_Error
388 */
389 public function create_field_group( $input = array() ) {
390 // Prepare the field group data.
391 $field_group_data = array(
392 'key' => 'group_' . uniqid(),
393 'title' => sanitize_text_field( $input['title'] ),
394 'fields' => $input['fields'],
395 'location' => $this->sanitize_location_rules( $input['location'] ),
396 'description' => isset( $input['description'] ) ? sanitize_textarea_field( $input['description'] ) : '',
397 'position' => $input['position'] ?? 'normal',
398 'style' => $input['style'] ?? 'default',
399 'label_placement' => $input['label_placement'] ?? 'top',
400 'instruction_placement' => $input['instruction_placement'] ?? 'label',
401 'hide_on_screen' => ! empty( $input['hide_on_screen'] ) ? $input['hide_on_screen'] : array(),
402 'active' => ! isset( $input['active'] ) || $input['active'],
403 'show_in_rest' => ! isset( $input['show_in_rest'] ) || $input['show_in_rest'],
404 'allow_ai_access' => ! isset( $input['allow_ai_access'] ) || $input['allow_ai_access'],
405 'ai_description' => isset( $input['ai_description'] ) ? sanitize_text_field( $input['ai_description'] ) : '',
406 );
407
408 // Create the field group using ACF's function.
409 add_filter( 'acf/prepare_field_for_import', array( $this, 'prepare_field_for_ability_import' ), 5 );
410 $field_group = acf_import_field_group( $field_group_data );
411 remove_filter( 'acf/prepare_field_for_import', array( $this, 'prepare_field_for_ability_import' ), 5 );
412
413 if ( empty( $field_group['ID'] ) || ! is_int( $field_group['ID'] ) ) {
414 return new WP_Error(
415 'field_group_creation_failed',
416 __( 'Failed to create the field group', 'acf' ),
417 array( 'field_group_data' => $field_group )
418 );
419 }
420
421 return array(
422 'success' => true,
423 'field_group' => $field_group,
424 'field_group_id' => $field_group['ID'],
425 'message' => sprintf(
426 /* translators: %s: Field group title */
427 __( 'Field group "%s" created successfully.', 'acf' ),
428 $field_group['title']
429 ),
430 );
431 }
432
433 /**
434 * Ensures a field has a key and name before import and sanitizes user input.
435 *
436 * @since 6.8.0
437 *
438 * @param array $field The field being prepared for import.
439 * @return array The field with key, name, and sanitized values.
440 */
441 public function prepare_field_for_ability_import( $field ) {
442 // Generate field name if not provided.
443 if ( empty( $field['name'] ) && ! empty( $field['label'] ) ) {
444 $field['name'] = acf_slugify( $field['label'], '_' );
445 }
446
447 // Generate field key if not provided.
448 if ( empty( $field['key'] ) ) {
449 $field['key'] = 'field_' . uniqid();
450 }
451
452 if ( ! empty( $field['label'] ) ) {
453 $field['label'] = sanitize_text_field( $field['label'] );
454 }
455
456 if ( ! empty( $field['instructions'] ) ) {
457 $field['instructions'] = wp_kses_post( $field['instructions'] );
458 }
459
460 if ( ! empty( $field['placeholder'] ) ) {
461 $field['placeholder'] = sanitize_text_field( $field['placeholder'] );
462 }
463
464 if ( ! empty( $field['prepend'] ) ) {
465 $field['prepend'] = sanitize_text_field( $field['prepend'] );
466 }
467
468 if ( ! empty( $field['append'] ) ) {
469 $field['append'] = sanitize_text_field( $field['append'] );
470 }
471
472 if ( ! empty( $field['wrapper']['class'] ) ) {
473 $field['wrapper']['class'] = sanitize_text_field( $field['wrapper']['class'] );
474 }
475
476 if ( ! empty( $field['wrapper']['id'] ) ) {
477 $field['wrapper']['id'] = sanitize_key( $field['wrapper']['id'] );
478 }
479
480 if ( ! empty( $field['choices'] ) && is_array( $field['choices'] ) ) {
481 $field['choices'] = array_map( 'sanitize_text_field', $field['choices'] );
482 }
483
484 if ( isset( $field['min'] ) ) {
485 $field['min'] = absint( $field['min'] );
486 }
487
488 if ( isset( $field['max'] ) ) {
489 $field['max'] = absint( $field['max'] );
490 }
491
492 if ( isset( $field['default_value'] ) ) {
493 if ( is_string( $field['default_value'] ) ) {
494 $field['default_value'] = sanitize_text_field( $field['default_value'] );
495 } elseif ( is_array( $field['default_value'] ) ) {
496 $field['default_value'] = array_map( 'sanitize_text_field', $field['default_value'] );
497 }
498 }
499
500 if ( ! empty( $field['message'] ) ) {
501 $field['message'] = wp_kses_post( $field['message'] );
502 }
503
504 if ( ! empty( $field['conditional_logic'] ) && is_array( $field['conditional_logic'] ) ) {
505 $field['conditional_logic'] = $this->sanitize_conditional_logic( $field['conditional_logic'] );
506 }
507
508 return $field;
509 }
510
511 /**
512 * Sanitize location rules for a field group.
513 *
514 * @since 6.8.0
515 *
516 * @param array $location The location rules array.
517 * @return array The sanitized location rules.
518 */
519 private function sanitize_location_rules( array $location ): array {
520 foreach ( $location as &$group ) {
521 if ( ! is_array( $group ) ) {
522 continue;
523 }
524
525 foreach ( $group as &$rule ) {
526 if ( ! is_array( $rule ) ) {
527 continue;
528 }
529
530 if ( isset( $rule['param'] ) ) {
531 $rule['param'] = sanitize_text_field( $rule['param'] );
532 }
533
534 if ( isset( $rule['operator'] ) ) {
535 $rule['operator'] = sanitize_text_field( $rule['operator'] );
536 }
537
538 if ( isset( $rule['value'] ) ) {
539 $rule['value'] = sanitize_text_field( $rule['value'] );
540 }
541 }
542 }
543
544 return $location;
545 }
546
547 /**
548 * Sanitize conditional logic rules for a field.
549 *
550 * @since 6.8.0
551 *
552 * @param array $conditional_logic The conditional logic array.
553 * @return array The sanitized conditional logic.
554 */
555 private function sanitize_conditional_logic( array $conditional_logic ): array {
556 foreach ( $conditional_logic as &$group ) {
557 if ( ! is_array( $group ) ) {
558 continue;
559 }
560
561 foreach ( $group as &$rule ) {
562 if ( ! is_array( $rule ) ) {
563 continue;
564 }
565
566 if ( isset( $rule['field'] ) ) {
567 $rule['field'] = sanitize_text_field( $rule['field'] );
568 }
569
570 if ( isset( $rule['operator'] ) ) {
571 $rule['operator'] = sanitize_text_field( $rule['operator'] );
572 }
573
574 if ( isset( $rule['value'] ) ) {
575 $rule['value'] = sanitize_text_field( $rule['value'] );
576 }
577 }
578 }
579
580 return $conditional_logic;
581 }
582 }
583