PluginProbe ʕ •ᴥ•ʔ
Secure Custom Fields / 6.8.1
Secure Custom Fields v6.8.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 / abilities / class-scf-field-abilities.php
secure-custom-fields / includes / abilities Last commit date
class-scf-abilities-integration.php 6 months ago class-scf-field-abilities.php 6 months ago class-scf-field-group-abilities.php 6 months ago class-scf-internal-post-type-abilities.php 6 months ago class-scf-post-type-abilities.php 7 months ago class-scf-taxonomy-abilities.php 7 months ago class-scf-ui-options-page-abilities.php 6 months ago
class-scf-field-abilities.php
786 lines
1 <?php
2 /**
3 * SCF Field Abilities
4 *
5 * Handles WordPress Abilities API registration for SCF field management.
6 * Unlike other entity types, fields use SCF_Field_Manager adapter instead of
7 * extending SCF_Internal_Post_Type_Abilities, as fields are nested under
8 * field groups and use a functional API.
9 *
10 * @package wordpress/secure-custom-fields
11 * @since 6.8.0
12 */
13
14 // Exit if accessed directly.
15 if ( ! defined( 'ABSPATH' ) ) {
16 exit;
17 }
18
19 if ( ! class_exists( 'SCF_Field_Abilities' ) ) :
20
21 /**
22 * SCF Field Abilities class.
23 *
24 * Registers and handles all field management abilities for the
25 * WordPress Abilities API integration. Provides programmatic access
26 * to SCF field operations.
27 *
28 * @since 6.8.0
29 */
30 class SCF_Field_Abilities {
31
32 /**
33 * The field manager adapter instance.
34 *
35 * @var SCF_Field_Manager
36 */
37 private $manager = null;
38
39 /**
40 * The field schema.
41 *
42 * @var array|null
43 */
44 private $field_schema = null;
45
46 /**
47 * The SCF identifier schema.
48 *
49 * @var array|null
50 */
51 private $scf_identifier_schema = null;
52
53 /**
54 * Constructor.
55 *
56 * @since 6.8.0
57 */
58 public function __construct() {
59 $validator = acf_get_instance( 'SCF_JSON_Schema_Validator' );
60 if ( ! $validator->validate_required_schemas() ) {
61 return;
62 }
63
64 add_action( 'wp_abilities_api_categories_init', array( $this, 'register_categories' ) );
65 add_action( 'wp_abilities_api_init', array( $this, 'register_abilities' ) );
66 }
67
68 /**
69 * Gets the field manager instance.
70 *
71 * @since 6.8.0
72 *
73 * @return SCF_Field_Manager
74 */
75 private function manager() {
76 if ( null === $this->manager ) {
77 $this->manager = new SCF_Field_Manager();
78 }
79 return $this->manager;
80 }
81
82 /**
83 * Gets the ability category name.
84 *
85 * @since 6.8.0
86 *
87 * @return string
88 */
89 private function ability_category() {
90 return 'scf-fields';
91 }
92
93 /**
94 * Generates an ability name.
95 *
96 * @since 6.8.0
97 *
98 * @param string $action The action (list, get, create, etc.).
99 * @return string E.g., 'scf/list-fields', 'scf/get-field'.
100 */
101 private function ability_name( $action ) {
102 $suffix = 'list' === $action ? 'fields' : 'field';
103 return 'scf/' . $action . '-' . $suffix;
104 }
105
106 /**
107 * Gets the composed field schema with oneOf variants for each field type.
108 *
109 * Loads the generated field.schema.json and resolves internal refs
110 * (like conditionalLogicGroup) for WordPress Abilities API compatibility.
111 *
112 * @since 6.8.0
113 *
114 * @return array
115 */
116 private function get_field_schema() {
117 if ( null === $this->field_schema ) {
118 $schema_path = ACF_PATH . 'schemas/field.schema.json';
119 $schema_content = file_get_contents( $schema_path );
120 $schema = json_decode( $schema_content, true );
121 $field_def = $schema['definitions']['field'] ?? array();
122
123 // Resolve internal refs (conditionalLogicGroup, etc.) at runtime.
124 $builder = acf_get_instance( 'SCF_Schema_Builder' );
125 $this->field_schema = $builder->resolve_refs( $field_def, $schema );
126 }
127 return $this->field_schema;
128 }
129
130 /**
131 * Gets the SCF identifier schema.
132 *
133 * @since 6.8.0
134 *
135 * @return array
136 */
137 private function get_scf_identifier_schema() {
138 if ( null === $this->scf_identifier_schema ) {
139 $schema_path = ACF_PATH . 'schemas/scf-identifier.schema.json';
140 $schema_content = file_get_contents( $schema_path );
141 $this->scf_identifier_schema = json_decode( $schema_content, true );
142 }
143 return $this->scf_identifier_schema;
144 }
145
146 /**
147 * Gets the internal fields schema for fields.
148 *
149 * Resolves $ref references since WordPress REST API doesn't understand them.
150 *
151 * @since 6.8.0
152 *
153 * @return array
154 */
155 private function get_internal_fields_schema() {
156 $validator = new SCF_JSON_Schema_Validator();
157 $schema = $validator->load_schema( 'internal-properties' );
158 $schema_array = json_decode( wp_json_encode( $schema ), true );
159 $field_properties = $schema_array['definitions']['fieldInternalProperties'] ?? array();
160
161 // Resolve $refs for WordPress REST API compatibility.
162 $builder = acf_get_instance( 'SCF_Schema_Builder' );
163 return $builder->resolve_refs( $field_properties, $schema_array );
164 }
165
166 /**
167 * Gets the field schema merged with internal fields.
168 *
169 * Used for output schemas of GET/LIST/CREATE/UPDATE/DUPLICATE abilities.
170 * Export uses get_field_schema() directly (no internal fields).
171 *
172 * The composed field schema uses oneOf at the top level with each variant
173 * containing merged base + type-specific properties. Internal fields are
174 * merged into each variant's properties.
175 *
176 * @since 6.8.0
177 *
178 * @return array
179 */
180 private function get_field_with_internal_fields_schema() {
181 $schema = $this->get_field_schema();
182 $internal = $this->get_internal_fields_schema();
183
184 // Merge internal fields into each oneOf variant's properties.
185 if ( isset( $schema['oneOf'] ) ) {
186 foreach ( $schema['oneOf'] as &$variant ) {
187 if ( isset( $variant['properties'] ) ) {
188 $variant['properties'] = array_merge(
189 $variant['properties'],
190 $internal['properties']
191 );
192 }
193 }
194 unset( $variant );
195 }
196
197 return $schema;
198 }
199
200 /**
201 * Registers ability categories.
202 *
203 * @since 6.8.0
204 */
205 public function register_categories() {
206 wp_register_ability_category(
207 $this->ability_category(),
208 array(
209 'label' => __( 'SCF Fields', 'secure-custom-fields' ),
210 'description' => __( 'Abilities for managing Secure Custom Fields fields.', 'secure-custom-fields' ),
211 )
212 );
213 }
214
215 /**
216 * Registers all field abilities.
217 *
218 * @since 6.8.0
219 */
220 public function register_abilities() {
221 $this->register_list_ability();
222 $this->register_get_ability();
223 $this->register_create_ability();
224 $this->register_update_ability();
225 $this->register_delete_ability();
226 $this->register_duplicate_ability();
227 $this->register_export_ability();
228 $this->register_import_ability();
229 }
230
231 /**
232 * Registers the list ability.
233 *
234 * @since 6.8.0
235 */
236 private function register_list_ability() {
237 wp_register_ability(
238 $this->ability_name( 'list' ),
239 array(
240 'label' => __( 'List Fields', 'secure-custom-fields' ),
241 'description' => __( 'Retrieves a list of SCF fields. Returns all if no filter provided. Can filter by parent (field group), type, or name.', 'secure-custom-fields' ),
242 'category' => $this->ability_category(),
243 'execute_callback' => array( $this, 'list_callback' ),
244 'permission_callback' => 'scf_current_user_has_capability',
245 'meta' => array(
246 'show_in_rest' => true,
247 'mcp' => array( 'public' => true ),
248 'annotations' => array(
249 'readonly' => true,
250 'destructive' => false,
251 'idempotent' => true,
252 ),
253 ),
254 'input_schema' => array(
255 'type' => 'object',
256 'properties' => array(
257 'filter' => array(
258 'type' => 'object',
259 'description' => __( 'Filter fields by parent, type, or name.', 'secure-custom-fields' ),
260 'properties' => array(
261 'parent' => array(
262 'type' => array( 'integer', 'string' ),
263 'description' => __( 'Field group ID or key to filter by.', 'secure-custom-fields' ),
264 ),
265 'type' => array(
266 'type' => 'string',
267 'description' => __( 'Field type to filter by (e.g., text, image).', 'secure-custom-fields' ),
268 ),
269 'name' => array(
270 'type' => 'string',
271 'description' => __( 'Field name to filter by.', 'secure-custom-fields' ),
272 ),
273 ),
274 ),
275 ),
276 ),
277 'output_schema' => array(
278 'type' => 'array',
279 'items' => $this->get_field_with_internal_fields_schema(),
280 ),
281 )
282 );
283 }
284
285 /**
286 * Registers the get ability.
287 *
288 * @since 6.8.0
289 */
290 private function register_get_ability() {
291 wp_register_ability(
292 $this->ability_name( 'get' ),
293 array(
294 'label' => __( 'Get Field', 'secure-custom-fields' ),
295 'description' => __( 'Retrieves a single SCF field by key or ID.', 'secure-custom-fields' ),
296 'category' => $this->ability_category(),
297 'execute_callback' => array( $this, 'get_callback' ),
298 'permission_callback' => 'scf_current_user_has_capability',
299 'meta' => array(
300 'show_in_rest' => true,
301 'mcp' => array( 'public' => true ),
302 'annotations' => array(
303 'readonly' => true,
304 'destructive' => false,
305 'idempotent' => true,
306 ),
307 ),
308 'input_schema' => array(
309 'type' => 'object',
310 'required' => array( 'identifier' ),
311 'properties' => array(
312 'identifier' => $this->get_scf_identifier_schema(),
313 ),
314 ),
315 'output_schema' => $this->get_field_with_internal_fields_schema(),
316 )
317 );
318 }
319
320 /**
321 * Registers the create ability.
322 *
323 * @since 6.8.0
324 */
325 private function register_create_ability() {
326 wp_register_ability(
327 $this->ability_name( 'create' ),
328 array(
329 'label' => __( 'Create Field', 'secure-custom-fields' ),
330 'description' => __( 'Creates a new SCF field with provided configuration. Requires parent (field group ID).', 'secure-custom-fields' ),
331 'category' => $this->ability_category(),
332 'execute_callback' => array( $this, 'create_callback' ),
333 'permission_callback' => 'scf_current_user_has_capability',
334 'meta' => array(
335 'show_in_rest' => true,
336 'mcp' => array( 'public' => true ),
337 'annotations' => array(
338 'readonly' => false,
339 'destructive' => false,
340 'idempotent' => false,
341 ),
342 ),
343 'input_schema' => $this->get_field_schema(),
344 'output_schema' => $this->get_field_with_internal_fields_schema(),
345 )
346 );
347 }
348
349 /**
350 * Registers the update ability.
351 *
352 * @since 6.8.0
353 */
354 private function register_update_ability() {
355 // Get field properties from field schema.
356 $field_schema = $this->get_field_schema();
357 $field_properties = array();
358 if ( isset( $field_schema['definitions']['field']['properties'] ) ) {
359 $field_properties = $field_schema['definitions']['field']['properties'];
360 }
361
362 wp_register_ability(
363 $this->ability_name( 'update' ),
364 array(
365 'label' => __( 'Update Field', 'secure-custom-fields' ),
366 'description' => __( 'Updates an existing SCF field. Properties not provided are preserved (merge behavior).', 'secure-custom-fields' ),
367 'category' => $this->ability_category(),
368 'execute_callback' => array( $this, 'update_callback' ),
369 'permission_callback' => 'scf_current_user_has_capability',
370 'meta' => array(
371 'show_in_rest' => true,
372 'mcp' => array( 'public' => true ),
373 'annotations' => array(
374 'readonly' => false,
375 'destructive' => false,
376 'idempotent' => true,
377 ),
378 ),
379 'input_schema' => array(
380 'type' => 'object',
381 'required' => array( 'ID' ),
382 'properties' => array_merge(
383 array(
384 'ID' => array(
385 'type' => 'integer',
386 'description' => __( 'The field ID.', 'secure-custom-fields' ),
387 ),
388 ),
389 $field_properties
390 ),
391 ),
392 'output_schema' => $this->get_field_with_internal_fields_schema(),
393 )
394 );
395 }
396
397 /**
398 * Registers the delete ability.
399 *
400 * @since 6.8.0
401 */
402 private function register_delete_ability() {
403 wp_register_ability(
404 $this->ability_name( 'delete' ),
405 array(
406 'label' => __( 'Delete Field', 'secure-custom-fields' ),
407 'description' => __( 'Permanently deletes an SCF field.', 'secure-custom-fields' ),
408 'category' => $this->ability_category(),
409 'execute_callback' => array( $this, 'delete_callback' ),
410 'permission_callback' => 'scf_current_user_has_capability',
411 'meta' => array(
412 'show_in_rest' => true,
413 'mcp' => array( 'public' => true ),
414 'annotations' => array(
415 'readonly' => false,
416 'destructive' => true,
417 'idempotent' => true,
418 ),
419 ),
420 'input_schema' => array(
421 'type' => 'object',
422 'required' => array( 'identifier' ),
423 'properties' => array(
424 'identifier' => $this->get_scf_identifier_schema(),
425 ),
426 ),
427 'output_schema' => array(
428 'type' => 'boolean',
429 ),
430 )
431 );
432 }
433
434 /**
435 * Registers the duplicate ability.
436 *
437 * @since 6.8.0
438 */
439 private function register_duplicate_ability() {
440 wp_register_ability(
441 $this->ability_name( 'duplicate' ),
442 array(
443 'label' => __( 'Duplicate Field', 'secure-custom-fields' ),
444 'description' => __( 'Creates a copy of an SCF field with a new unique key. Optionally specify a new parent field group.', 'secure-custom-fields' ),
445 'category' => $this->ability_category(),
446 'execute_callback' => array( $this, 'duplicate_callback' ),
447 'permission_callback' => 'scf_current_user_has_capability',
448 'meta' => array(
449 'show_in_rest' => true,
450 'mcp' => array( 'public' => true ),
451 'annotations' => array(
452 'readonly' => false,
453 'destructive' => false,
454 'idempotent' => false,
455 ),
456 ),
457 'input_schema' => array(
458 'type' => 'object',
459 'required' => array( 'identifier' ),
460 'properties' => array(
461 'identifier' => $this->get_scf_identifier_schema(),
462 'new_parent_id' => array(
463 'type' => 'integer',
464 'description' => __( 'Optional field group ID to place the duplicate in.', 'secure-custom-fields' ),
465 ),
466 ),
467 ),
468 'output_schema' => $this->get_field_with_internal_fields_schema(),
469 )
470 );
471 }
472
473 /**
474 * Registers the export ability.
475 *
476 * @since 6.8.0
477 */
478 private function register_export_ability() {
479 wp_register_ability(
480 $this->ability_name( 'export' ),
481 array(
482 'label' => __( 'Export Field', 'secure-custom-fields' ),
483 'description' => __( 'Exports an SCF field as JSON for backup or transfer. Internal fields (ID, local, _valid) are stripped.', 'secure-custom-fields' ),
484 'category' => $this->ability_category(),
485 'execute_callback' => array( $this, 'export_callback' ),
486 'permission_callback' => 'scf_current_user_has_capability',
487 'meta' => array(
488 'show_in_rest' => true,
489 'mcp' => array( 'public' => true ),
490 'annotations' => array(
491 'readonly' => true,
492 'destructive' => false,
493 'idempotent' => true,
494 ),
495 ),
496 'input_schema' => array(
497 'type' => 'object',
498 'required' => array( 'identifier' ),
499 'properties' => array(
500 'identifier' => $this->get_scf_identifier_schema(),
501 ),
502 ),
503 'output_schema' => $this->get_field_schema(),
504 )
505 );
506 }
507
508 /**
509 * Registers the import ability.
510 *
511 * @since 6.8.0
512 */
513 private function register_import_ability() {
514 wp_register_ability(
515 $this->ability_name( 'import' ),
516 array(
517 'label' => __( 'Import Field', 'secure-custom-fields' ),
518 'description' => __( 'Imports an SCF field from JSON data. If key exists, updates existing; otherwise creates new.', 'secure-custom-fields' ),
519 'category' => $this->ability_category(),
520 'execute_callback' => array( $this, 'import_callback' ),
521 'permission_callback' => 'scf_current_user_has_capability',
522 'meta' => array(
523 'show_in_rest' => true,
524 'mcp' => array( 'public' => true ),
525 'annotations' => array(
526 'readonly' => false,
527 'destructive' => false,
528 'idempotent' => true,
529 ),
530 ),
531 'input_schema' => $this->get_field_schema(),
532 'output_schema' => $this->get_field_with_internal_fields_schema(),
533 )
534 );
535 }
536
537 /**
538 * Handles the list ability callback.
539 *
540 * @since 6.8.0
541 *
542 * @param array $input The input parameters.
543 * @return array List of fields.
544 */
545 public function list_callback( $input ) {
546 $filter = isset( $input['filter'] ) && is_array( $input['filter'] ) ? $input['filter'] : array();
547 return $this->manager()->filter_posts( $this->manager()->get_posts(), $filter );
548 }
549
550 /**
551 * Handles the get ability callback.
552 *
553 * @since 6.8.0
554 *
555 * @param array $input The input parameters.
556 * @return array|WP_Error Field data or error if not found.
557 */
558 public function get_callback( $input ) {
559 $field = $this->manager()->get_post( $input['identifier'] );
560 if ( ! $field ) {
561 return $this->not_found_error();
562 }
563 return $field;
564 }
565
566 /**
567 * Handles the create ability callback.
568 *
569 * @since 6.8.0
570 *
571 * @param array $input The field data to create.
572 * @return array|WP_Error Created field or error on failure.
573 */
574 public function create_callback( $input ) {
575 // Check for existing field with same key.
576 if ( isset( $input['key'] ) && $this->manager()->get_post( $input['key'] ) ) {
577 return new WP_Error(
578 'already_exists',
579 __( 'Field with this key already exists.', 'secure-custom-fields' )
580 );
581 }
582
583 if ( ! $this->parent_exists( $input['parent'] ) ) {
584 return new WP_Error(
585 'parent_not_found',
586 __( 'Parent field group or field does not exist.', 'secure-custom-fields' ),
587 array( 'status' => 400 )
588 );
589 }
590
591 $field = $this->manager()->update_post( $input );
592 if ( ! $field ) {
593 return new WP_Error(
594 'create_failed',
595 __( 'Failed to create field.', 'secure-custom-fields' )
596 );
597 }
598 return $field;
599 }
600
601 /**
602 * Handles the update ability callback.
603 *
604 * @since 6.8.0
605 *
606 * @param array $input The field data to update.
607 * @return array|WP_Error Updated field or error on failure.
608 */
609 public function update_callback( $input ) {
610 $existing = $this->manager()->get_post( $input['ID'] );
611 if ( ! $existing ) {
612 return $this->not_found_error();
613 }
614
615 // Merge with existing data.
616 $updated_data = array_merge( $existing, $input );
617 $field = $this->manager()->update_post( $updated_data );
618
619 if ( ! $field ) {
620 return new WP_Error(
621 'update_failed',
622 __( 'Failed to update field.', 'secure-custom-fields' )
623 );
624 }
625 return $field;
626 }
627
628 /**
629 * Handles the delete ability callback.
630 *
631 * @since 6.8.0
632 *
633 * @param array $input The input parameters.
634 * @return bool|WP_Error True on success or error on failure.
635 */
636 public function delete_callback( $input ) {
637 if ( ! $this->manager()->get_post( $input['identifier'] ) ) {
638 return $this->not_found_error();
639 }
640
641 if ( ! $this->manager()->delete_post( $input['identifier'] ) ) {
642 return new WP_Error(
643 'delete_failed',
644 __( 'Failed to delete field.', 'secure-custom-fields' )
645 );
646 }
647 return true;
648 }
649
650 /**
651 * Handles the duplicate ability callback.
652 *
653 * @since 6.8.0
654 *
655 * @param array $input The input parameters.
656 * @return array|WP_Error Duplicated field or error on failure.
657 */
658 public function duplicate_callback( $input ) {
659 if ( ! $this->manager()->get_post( $input['identifier'] ) ) {
660 return $this->not_found_error();
661 }
662
663 $new_parent_id = isset( $input['new_parent_id'] ) ? $input['new_parent_id'] : 0;
664
665 // Validate that new_parent_id references an existing parent (field group or parent field).
666 if ( $new_parent_id && ! $this->parent_exists( $new_parent_id ) ) {
667 return new WP_Error(
668 'invalid_new_parent_id',
669 sprintf(
670 /* translators: %d: Invalid parent ID */
671 __( 'Invalid new_parent_id: %d is not a valid field group or parent field.', 'secure-custom-fields' ),
672 $new_parent_id
673 ),
674 array( 'status' => 400 )
675 );
676 }
677
678 $duplicated = $this->manager()->duplicate_post( $input['identifier'], $new_parent_id );
679
680 if ( ! $duplicated ) {
681 return new WP_Error(
682 'duplicate_failed',
683 __( 'Failed to duplicate field.', 'secure-custom-fields' )
684 );
685 }
686 return $duplicated;
687 }
688
689 /**
690 * Handles the export ability callback.
691 *
692 * @since 6.8.0
693 *
694 * @param array $input The input parameters.
695 * @return array|WP_Error Exported field data or error on failure.
696 */
697 public function export_callback( $input ) {
698 $field = $this->manager()->get_post( $input['identifier'] );
699 if ( ! $field ) {
700 return $this->not_found_error();
701 }
702
703 $export = $this->manager()->prepare_post_for_export( $field );
704 if ( ! $export ) {
705 return new WP_Error(
706 'export_failed',
707 __( 'Failed to export field.', 'secure-custom-fields' )
708 );
709 }
710 return $export;
711 }
712
713 /**
714 * Handles the import ability callback.
715 *
716 * @since 6.8.0
717 *
718 * @param array|object $input The field data to import.
719 * @return array|WP_Error Imported field or error on failure.
720 */
721 public function import_callback( $input ) {
722 if ( ! $this->parent_exists( $input['parent'] ) ) {
723 return new WP_Error(
724 'parent_not_found',
725 __( 'Parent field group or field does not exist.', 'secure-custom-fields' ),
726 array( 'status' => 400 )
727 );
728 }
729
730 $imported = $this->manager()->import_post( $input );
731 if ( ! $imported ) {
732 return new WP_Error(
733 'import_failed',
734 __( 'Failed to import field.', 'secure-custom-fields' )
735 );
736 }
737 return $imported;
738 }
739
740 /**
741 * Checks if the parent field group or field exists.
742 *
743 * @since 6.8.0
744 *
745 * @param int|string $parent_id The parent ID or key.
746 * @return bool True if parent exists, false otherwise.
747 */
748 private function parent_exists( $parent_id ) {
749 /**
750 * Filters the result of the parent existence check.
751 *
752 * @since 6.8.0
753 *
754 * @param bool|null $exists The existence result. Null to use default logic.
755 * @param int|string $parent_id The parent ID or key being checked.
756 */
757 $filtered = apply_filters( 'scf_field_parent_exists', null, $parent_id );
758 if ( null !== $filtered ) {
759 return (bool) $filtered;
760 }
761
762 // Parent can be a field group or a parent field (for sub-fields).
763 return (bool) acf_get_field_group( $parent_id ) || (bool) acf_get_field( $parent_id );
764 }
765
766 /**
767 * Creates a not found error response.
768 *
769 * @since 6.8.0
770 *
771 * @return WP_Error
772 */
773 private function not_found_error() {
774 return new WP_Error(
775 'not_found',
776 __( 'Field not found.', 'secure-custom-fields' ),
777 array( 'status' => 404 )
778 );
779 }
780 }
781
782 // Initialize abilities instance.
783 acf_new_instance( 'SCF_Field_Abilities' );
784
785 endif; // class_exists check.
786