PluginProbe ʕ •ᴥ•ʔ
Atarim – AI Agency for WordPress: Edit Pages, Fix Code, Update Plugins, SEO & Client Feedback / 5.0
Atarim – AI Agency for WordPress: Edit Pages, Fix Code, Update Plugins, SEO & Client Feedback v5.0
5.1.3 5.1.2 5.1.1 5.1 5.0 trunk 3.10 3.11 3.12 3.13 3.14 3.15 3.16 3.17 3.18 3.19 3.2.0 3.2.1 3.22 3.22.1 3.22.2 3.22.3 3.22.4 3.22.5 3.22.6 3.3.0 3.3.1 3.3.2 3.3.2.1 3.3.2.2 3.3.3 3.30 3.31 3.32 3.4 3.4.1 3.4.3 3.4.4 3.5 3.5.1 3.6 3.6.1 3.7 3.8 3.9 3.9.1 3.9.2 3.9.3 3.9.4 3.9.6 3.9.6.1 4.0 4.0.1 4.0.2 4.0.3 4.0.4 4.0.5 4.0.6 4.0.7 4.0.8 4.0.9 4.1.0 4.1.1 4.1.2 4.1.3 4.2 4.2.1 4.2.2 4.3 4.3.1 4.3.2 4.3.3 4.3.4 4.3.5 4.4
atarim-visual-collaboration / third-party / content-model / acf / class-avcf-acf-field-groups.php
atarim-visual-collaboration / third-party / content-model / acf Last commit date
class-avcf-acf-abilities.php 1 month ago class-avcf-acf-content-models.php 1 month ago class-avcf-acf-detector.php 1 month ago class-avcf-acf-field-groups.php 1 month ago class-avcf-acf-helpers.php 1 month ago
class-avcf-acf-field-groups.php
400 lines
1 <?php
2 /**
3 * ACF — Field Group MCP abilities.
4 *
5 * Manages ACF field group *structure* (the groups and their fields), not the
6 * per-post values — value read/write is handled by the generic metadata
7 * abilities (atarim/get-post-field, atarim/update-post-field) which already
8 * route through ACF. This cluster is the authoring layer: introspect what
9 * fields exist and create/modify the definitions themselves.
10 *
11 * All writes go through acf_import_field_group(), which upserts by key, so
12 * ACF's own validation, key handling, and cache invalidation run as expected.
13 * Only database-stored groups are mutable; PHP- and JSON-registered groups
14 * are reported read-only (see AVCF_ACF_Helpers::is_mutable()).
15 *
16 * Exposed abilities:
17 * atarim/list-acf-field-groups Enumerate groups (summary + storage mode).
18 * atarim/get-acf-field-group Full schema for one group incl. nested fields.
19 * atarim/create-acf-field-group New DB-stored group with fields + location.
20 * atarim/update-acf-field-group Patch title/active/location/fields of a DB group.
21 * atarim/delete-acf-field-group Delete a DB group and cascade its fields.
22 *
23 * @package atarim-visual-collaboration
24 */
25
26 if ( ! defined('ABSPATH') ) {
27 exit;
28 }
29
30 class AVCF_ACF_Field_Groups extends AVCF_Abilities_Base {
31
32 /**
33 * @var AVCF_ACF_Detector
34 */
35 private $detector;
36
37 public function __construct() {
38 $this->detector = new AVCF_ACF_Detector();
39 }
40
41 public function register() {
42 if ( ! $this->detector->avcf_acf_is_available() ) {
43 return;
44 }
45
46 // ---- list-acf-field-groups ----
47 wp_register_ability( 'atarim/list-acf-field-groups', [
48 'label' => 'List ACF Field Groups',
49 'description' => 'Enumerate all ACF field groups on the site. Returns a summary per group: key, title, active state, storage mode (db / php / json), whether it is mutable (only db groups can be edited or deleted via these abilities), location rules, and field count. Use get-acf-field-group for the full field schema of one group.',
50 'category' => 'atarim',
51 'input_schema' => [
52 'type' => 'object',
53 'properties' => [
54 'active_only' => [
55 'type' => 'boolean',
56 'description' => 'Only return active field groups. Defaults to false (all groups).',
57 'default' => false,
58 ],
59 ],
60 'additionalProperties' => false,
61 ],
62 'output_schema' => [
63 'type' => 'object',
64 'properties' => [
65 'success' => [ 'type' => 'boolean' ],
66 'total' => [ 'type' => 'integer' ],
67 'groups' => [ 'type' => 'array' ],
68 'message' => [ 'type' => 'string' ],
69 ],
70 'required' => [ 'success', 'message' ],
71 ],
72 'execute_callback' => function( $input = [] ) {
73 if ( ! function_exists( 'acf_get_field_groups' ) ) {
74 return [ 'success' => false, 'message' => 'ACF is not available.' ];
75 }
76 $active_only = ! empty( $input['active_only'] );
77 $groups = acf_get_field_groups();
78 $out = [];
79 foreach ( (array) $groups as $group ) {
80 if ( $active_only && isset( $group['active'] ) && ! $group['active'] ) {
81 continue;
82 }
83 $out[] = AVCF_ACF_Helpers::group_summary( $group );
84 }
85 return [
86 'success' => true,
87 'total' => count( $out ),
88 'groups' => $out,
89 'message' => 'OK.',
90 ];
91 },
92 'permission_callback' => function() {
93 return current_user_can( 'manage_options' );
94 },
95 'meta' => [
96 'mcp' => [ 'public' => true, 'type' => 'tool' ],
97 'annotations' => [ 'readonly' => true, 'destructive' => false, 'idempotent' => true ],
98 ],
99 ] );
100
101 // ---- get-acf-field-group ----
102 wp_register_ability( 'atarim/get-acf-field-group', [
103 'label' => 'Get ACF Field Group',
104 'description' => 'Full schema for one field group by key (e.g. "group_5f8a1b2c3d4e5"): title, active, location rules, settings, and the complete field tree — including nested sub_fields (repeater/group) and flexible-content layouts. Call this before update-acf-field-group so you know the existing field keys to preserve.',
105 'category' => 'atarim',
106 'input_schema' => [
107 'type' => 'object',
108 'properties' => [
109 'key' => [
110 'type' => 'string',
111 'description' => 'Field group key, as returned by list-acf-field-groups (starts with "group_").',
112 'pattern' => '^group_',
113 ],
114 ],
115 'required' => [ 'key' ],
116 'additionalProperties' => false,
117 ],
118 'output_schema' => [
119 'type' => 'object',
120 'properties' => [
121 'success' => [ 'type' => 'boolean' ],
122 'group' => [ 'type' => 'object' ],
123 'message' => [ 'type' => 'string' ],
124 ],
125 'required' => [ 'success', 'message' ],
126 ],
127 'execute_callback' => function( $input = [] ) {
128 if ( ! function_exists( 'acf_get_field_group' ) ) {
129 return [ 'success' => false, 'message' => 'ACF is not available.' ];
130 }
131 $key = isset( $input['key'] ) ? (string) $input['key'] : '';
132 if ( $key === '' ) {
133 return [ 'success' => false, 'message' => 'key is required.' ];
134 }
135 $group = acf_get_field_group( $key );
136 if ( ! $group ) {
137 return [ 'success' => false, 'message' => sprintf( 'Field group "%s" not found.', $key ) ];
138 }
139 $fields = function_exists( 'acf_get_fields' ) ? (array) acf_get_fields( $key ) : [];
140 return [
141 'success' => true,
142 'group' => [
143 'key' => isset( $group['key'] ) ? (string) $group['key'] : $key,
144 'title' => isset( $group['title'] ) ? (string) $group['title'] : '',
145 'active' => isset( $group['active'] ) ? (bool) $group['active'] : true,
146 'storage' => AVCF_ACF_Helpers::storage_mode( $group ),
147 'mutable' => AVCF_ACF_Helpers::is_mutable( $group ),
148 'location' => isset( $group['location'] ) ? $group['location'] : [],
149 'menu_order' => isset( $group['menu_order'] ) ? (int) $group['menu_order'] : 0,
150 'position' => isset( $group['position'] ) ? (string) $group['position'] : 'normal',
151 'style' => isset( $group['style'] ) ? (string) $group['style'] : 'default',
152 'fields' => array_map( [ 'AVCF_ACF_Helpers', 'field_schema' ], $fields ),
153 ],
154 'message' => 'OK.',
155 ];
156 },
157 'permission_callback' => function() {
158 return current_user_can( 'manage_options' );
159 },
160 'meta' => [
161 'mcp' => [ 'public' => true, 'type' => 'tool' ],
162 'annotations' => [ 'readonly' => true, 'destructive' => false, 'idempotent' => true ],
163 ],
164 ] );
165
166 // ---- create-acf-field-group ----
167 wp_register_ability( 'atarim/create-acf-field-group', [
168 'label' => 'Create ACF Field Group',
169 'description' => 'Create a new database-stored ACF field group with fields and location rules. Field keys are generated automatically if omitted — never invent "field_" keys yourself. The group key is generated automatically. Location rules use ACF\'s standard nested structure: an array of OR-groups, each an array of AND-rules like {"param":"post_type","operator":"==","value":"page"}. Complex field types (repeater, group, flexible_content) require ACF Pro; their sub_fields/layouts are keyed recursively.',
170 'category' => 'atarim',
171 'input_schema' => [
172 'type' => 'object',
173 'properties' => [
174 'title' => [
175 'type' => 'string',
176 'description' => 'Human-readable field group title (e.g. "Page Hero").',
177 'minLength' => 1,
178 ],
179 'fields' => [
180 'type' => 'array',
181 'description' => 'Array of ACF field definitions. Each needs at least "label", "name", and "type". Keys are auto-generated. Repeater/group fields carry "sub_fields"; flexible_content carries "layouts".',
182 ],
183 'location' => [
184 'type' => 'array',
185 'description' => 'ACF location rule groups (array of arrays of {param, operator, value}). Defaults to no location (group registered but not attached) if omitted.',
186 ],
187 'active' => [
188 'type' => 'boolean',
189 'description' => 'Whether the group is active. Defaults to true.',
190 'default' => true,
191 ],
192 ],
193 'required' => [ 'title' ],
194 'additionalProperties' => false,
195 ],
196 'output_schema' => [
197 'type' => 'object',
198 'properties' => [
199 'success' => [ 'type' => 'boolean' ],
200 'key' => [ 'type' => 'string' ],
201 'message' => [ 'type' => 'string' ],
202 ],
203 'required' => [ 'success', 'message' ],
204 ],
205 'execute_callback' => function( $input = [] ) {
206 if ( ! function_exists( 'acf_import_field_group' ) ) {
207 return [ 'success' => false, 'message' => 'ACF is not available.' ];
208 }
209 $title = isset( $input['title'] ) ? trim( (string) $input['title'] ) : '';
210 if ( $title === '' ) {
211 return [ 'success' => false, 'message' => 'title is required.' ];
212 }
213 $fields = isset( $input['fields'] ) && is_array( $input['fields'] ) ? $input['fields'] : [];
214 $fields = AVCF_ACF_Helpers::ensure_field_keys( $fields );
215
216 $payload = [
217 'key' => AVCF_ACF_Helpers::generate_group_key(),
218 'title' => $title,
219 'fields' => $fields,
220 'location' => isset( $input['location'] ) && is_array( $input['location'] ) ? $input['location'] : [],
221 'active' => isset( $input['active'] ) ? (bool) $input['active'] : true,
222 ];
223
224 $result = acf_import_field_group( $payload );
225 if ( ! is_array( $result ) || empty( $result['key'] ) ) {
226 return [ 'success' => false, 'message' => 'ACF reported no key after import; create may have failed.' ];
227 }
228 return [
229 'success' => true,
230 'key' => (string) $result['key'],
231 'message' => sprintf( 'Created field group "%s".', $title ),
232 ];
233 },
234 'permission_callback' => function() {
235 return current_user_can( 'manage_options' );
236 },
237 'meta' => [
238 'mcp' => [ 'public' => true, 'type' => 'tool' ],
239 'annotations' => [ 'readonly' => false, 'destructive' => false, 'idempotent' => false ],
240 ],
241 ] );
242
243 // ---- update-acf-field-group ----
244 wp_register_ability( 'atarim/update-acf-field-group', [
245 'label' => 'Update ACF Field Group',
246 'description' => 'Patch an existing database-stored field group. Supply only the keys you want to change (title, active, location, fields). IMPORTANT: if you pass "fields", it REPLACES the entire field set — call get-acf-field-group first, modify the returned fields array, and pass it back whole, preserving existing "key" values so ACF updates fields in place rather than recreating them. Refuses on php/json-registered groups (they are read-only).',
247 'category' => 'atarim',
248 'input_schema' => [
249 'type' => 'object',
250 'properties' => [
251 'key' => [
252 'type' => 'string',
253 'description' => 'Field group key to update (starts with "group_").',
254 'pattern' => '^group_',
255 ],
256 'title' => [ 'type' => 'string', 'description' => 'New title.' ],
257 'active' => [ 'type' => 'boolean', 'description' => 'New active state.' ],
258 'location' => [ 'type' => 'array', 'description' => 'Replacement location rule groups.' ],
259 'fields' => [ 'type' => 'array', 'description' => 'Replacement field set (whole). Preserve existing keys to update in place.' ],
260 ],
261 'required' => [ 'key' ],
262 'additionalProperties' => false,
263 ],
264 'output_schema' => [
265 'type' => 'object',
266 'properties' => [
267 'success' => [ 'type' => 'boolean' ],
268 'key' => [ 'type' => 'string' ],
269 'message' => [ 'type' => 'string' ],
270 ],
271 'required' => [ 'success', 'message' ],
272 ],
273 'execute_callback' => function( $input = [] ) {
274 if ( ! function_exists( 'acf_get_field_group' ) || ! function_exists( 'acf_import_field_group' ) ) {
275 return [ 'success' => false, 'message' => 'ACF is not available.' ];
276 }
277 $key = isset( $input['key'] ) ? (string) $input['key'] : '';
278 if ( $key === '' ) {
279 return [ 'success' => false, 'message' => 'key is required.' ];
280 }
281 $existing = acf_get_field_group( $key );
282 if ( ! $existing ) {
283 return [ 'success' => false, 'message' => sprintf( 'Field group "%s" not found.', $key ) ];
284 }
285 if ( ! AVCF_ACF_Helpers::is_mutable( $existing ) ) {
286 return [ 'success' => false, 'message' => AVCF_ACF_Helpers::immutable_message( $existing, $key ) ];
287 }
288
289 // Start from the existing group; acf_import_field_group replaces
290 // wholesale, so unspecified keys must be carried forward.
291 $payload = $existing;
292 if ( isset( $input['title'] ) ) {
293 $payload['title'] = (string) $input['title'];
294 }
295 if ( isset( $input['active'] ) ) {
296 $payload['active'] = (bool) $input['active'];
297 }
298 if ( isset( $input['location'] ) && is_array( $input['location'] ) ) {
299 $payload['location'] = $input['location'];
300 }
301 if ( isset( $input['fields'] ) && is_array( $input['fields'] ) ) {
302 $payload['fields'] = AVCF_ACF_Helpers::ensure_field_keys( $input['fields'] );
303 } else {
304 // Preserve current fields when not replacing them.
305 $payload['fields'] = function_exists( 'acf_get_fields' ) ? (array) acf_get_fields( $key ) : [];
306 }
307
308 $result = acf_import_field_group( $payload );
309 if ( ! is_array( $result ) || empty( $result['key'] ) ) {
310 return [ 'success' => false, 'message' => 'ACF reported no key after import; update may have failed.' ];
311 }
312 return [
313 'success' => true,
314 'key' => (string) $result['key'],
315 'message' => sprintf( 'Updated field group "%s".', $key ),
316 ];
317 },
318 'permission_callback' => function() {
319 return current_user_can( 'manage_options' );
320 },
321 'meta' => [
322 'mcp' => [ 'public' => true, 'type' => 'tool' ],
323 'annotations' => [ 'readonly' => false, 'destructive' => false, 'idempotent' => false ],
324 ],
325 ] );
326
327 // ---- delete-acf-field-group ----
328 wp_register_ability( 'atarim/delete-acf-field-group', [
329 'label' => 'Delete ACF Field Group',
330 'description' => 'Delete a database-stored field group and its fields. This removes the field definitions only — existing post meta values written under those fields remain in the database untouched. Refuses on php/json-registered groups (read-only). Irreversible.',
331 'category' => 'atarim',
332 'input_schema' => [
333 'type' => 'object',
334 'properties' => [
335 'key' => [
336 'type' => 'string',
337 'description' => 'Field group key to delete (starts with "group_").',
338 'pattern' => '^group_',
339 ],
340 'confirm' => [
341 'type' => 'boolean',
342 'description' => 'Must be true to actually delete. Defaults to false (a dry run that reports what would be deleted).',
343 'default' => false,
344 ],
345 ],
346 'required' => [ 'key' ],
347 'additionalProperties' => false,
348 ],
349 'output_schema' => [
350 'type' => 'object',
351 'properties' => [
352 'success' => [ 'type' => 'boolean' ],
353 'deleted' => [ 'type' => 'boolean' ],
354 'message' => [ 'type' => 'string' ],
355 ],
356 'required' => [ 'success', 'message' ],
357 ],
358 'execute_callback' => function( $input = [] ) {
359 if ( ! function_exists( 'acf_get_field_group' ) || ! function_exists( 'acf_delete_field_group' ) ) {
360 return [ 'success' => false, 'message' => 'ACF is not available.' ];
361 }
362 $key = isset( $input['key'] ) ? (string) $input['key'] : '';
363 if ( $key === '' ) {
364 return [ 'success' => false, 'message' => 'key is required.' ];
365 }
366 $existing = acf_get_field_group( $key );
367 if ( ! $existing ) {
368 return [ 'success' => false, 'message' => sprintf( 'Field group "%s" not found.', $key ) ];
369 }
370 if ( ! AVCF_ACF_Helpers::is_mutable( $existing ) ) {
371 return [ 'success' => false, 'message' => AVCF_ACF_Helpers::immutable_message( $existing, $key ) ];
372 }
373 $title = isset( $existing['title'] ) ? (string) $existing['title'] : $key;
374 if ( empty( $input['confirm'] ) ) {
375 return [
376 'success' => true,
377 'deleted' => false,
378 'message' => sprintf( 'Dry run: would delete field group "%s" (%s). Re-call with confirm:true to delete.', $title, $key ),
379 ];
380 }
381 $deleted = acf_delete_field_group( $key );
382 return [
383 'success' => (bool) $deleted,
384 'deleted' => (bool) $deleted,
385 'message' => $deleted
386 ? sprintf( 'Deleted field group "%s".', $title )
387 : sprintf( 'ACF did not confirm deletion of "%s".', $key ),
388 ];
389 },
390 'permission_callback' => function() {
391 return current_user_can( 'manage_options' );
392 },
393 'meta' => [
394 'mcp' => [ 'public' => true, 'type' => 'tool' ],
395 'annotations' => [ 'readonly' => false, 'destructive' => true, 'idempotent' => false ],
396 ],
397 ] );
398 }
399 }
400