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 / 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 1 week ago class-scf-field-group-abilities.php 6 months ago class-scf-internal-post-type-abilities.php 1 week 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
882 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 all known field schema properties.
132 *
133 * The field schema may be either a direct object schema or a oneOf of
134 * field type variants. Update inputs are partial, so they need the union
135 * of known properties without requiring a complete field object.
136 *
137 * @since 6.8.0
138 *
139 * @return array
140 */
141 private function get_field_schema_properties() {
142 $field_schema = $this->get_field_schema();
143
144 if ( isset( $field_schema['definitions']['field'] ) ) {
145 $field_schema = $field_schema['definitions']['field'];
146 }
147
148 if ( isset( $field_schema['properties'] ) && is_array( $field_schema['properties'] ) ) {
149 return $field_schema['properties'];
150 }
151
152 $field_properties = array();
153 foreach ( $field_schema['oneOf'] ?? array() as $variant ) {
154 if ( isset( $variant['properties'] ) && is_array( $variant['properties'] ) ) {
155 $field_properties = $this->merge_field_schema_properties( $field_properties, $variant['properties'] );
156 }
157 }
158
159 return $field_properties;
160 }
161
162 /**
163 * Merges field property schema maps while preserving enum/type variants.
164 *
165 * @since 6.8.0
166 *
167 * @param array $properties Existing property schema map.
168 * @param array $next_properties Property schema map to merge in.
169 * @return array
170 */
171 private function merge_field_schema_properties( array $properties, array $next_properties ) {
172 foreach ( $next_properties as $property_name => $property_schema ) {
173 if ( ! isset( $properties[ $property_name ] ) || ! is_array( $properties[ $property_name ] ) || ! is_array( $property_schema ) ) {
174 $properties[ $property_name ] = $property_schema;
175 continue;
176 }
177
178 $properties[ $property_name ] = $this->merge_field_schema_property(
179 $properties[ $property_name ],
180 $property_schema
181 );
182 }
183
184 return $properties;
185 }
186
187 /**
188 * Merges two schemas for the same field property.
189 *
190 * @since 6.8.0
191 *
192 * @param array $property_schema Existing property schema.
193 * @param array $next_property_schema Property schema to merge in.
194 * @return array
195 */
196 private function merge_field_schema_property( array $property_schema, array $next_property_schema ) {
197 $merged = array_merge( $property_schema, $next_property_schema );
198
199 if ( isset( $property_schema['type'], $next_property_schema['type'] ) ) {
200 $merged['type'] = array_values(
201 array_unique(
202 array_merge(
203 (array) $property_schema['type'],
204 (array) $next_property_schema['type']
205 ),
206 SORT_REGULAR
207 )
208 );
209 }
210
211 if ( isset( $property_schema['enum'], $next_property_schema['enum'] ) && is_array( $property_schema['enum'] ) && is_array( $next_property_schema['enum'] ) ) {
212 $merged['enum'] = array_values(
213 array_unique(
214 array_merge( $property_schema['enum'], $next_property_schema['enum'] ),
215 SORT_REGULAR
216 )
217 );
218 }
219
220 if ( isset( $property_schema['properties'], $next_property_schema['properties'] ) && is_array( $property_schema['properties'] ) && is_array( $next_property_schema['properties'] ) ) {
221 $merged['properties'] = $this->merge_field_schema_properties(
222 $property_schema['properties'],
223 $next_property_schema['properties']
224 );
225 }
226
227 return $merged;
228 }
229
230 /**
231 * Gets the SCF identifier schema.
232 *
233 * @since 6.8.0
234 *
235 * @return array
236 */
237 private function get_scf_identifier_schema() {
238 if ( null === $this->scf_identifier_schema ) {
239 $schema_path = ACF_PATH . 'schemas/scf-identifier.schema.json';
240 $schema_content = file_get_contents( $schema_path );
241 $this->scf_identifier_schema = json_decode( $schema_content, true );
242 }
243 return $this->scf_identifier_schema;
244 }
245
246 /**
247 * Gets the internal fields schema for fields.
248 *
249 * Resolves $ref references since WordPress REST API doesn't understand them.
250 *
251 * @since 6.8.0
252 *
253 * @return array
254 */
255 private function get_internal_fields_schema() {
256 $validator = new SCF_JSON_Schema_Validator();
257 $schema = $validator->load_schema( 'internal-properties' );
258 $schema_array = json_decode( wp_json_encode( $schema ), true );
259 $field_properties = $schema_array['definitions']['fieldInternalProperties'] ?? array();
260
261 // Resolve $refs for WordPress REST API compatibility.
262 $builder = acf_get_instance( 'SCF_Schema_Builder' );
263 return $builder->resolve_refs( $field_properties, $schema_array );
264 }
265
266 /**
267 * Gets the field schema merged with internal fields.
268 *
269 * Used for output schemas of GET/LIST/CREATE/UPDATE/DUPLICATE abilities.
270 * Export uses get_field_schema() directly (no internal fields).
271 *
272 * The composed field schema uses oneOf at the top level with each variant
273 * containing merged base + type-specific properties. Internal fields are
274 * merged into each variant's properties.
275 *
276 * @since 6.8.0
277 *
278 * @return array
279 */
280 private function get_field_with_internal_fields_schema() {
281 $schema = $this->get_field_schema();
282 $internal = $this->get_internal_fields_schema();
283
284 // Merge internal fields into each oneOf variant's properties.
285 if ( isset( $schema['oneOf'] ) ) {
286 foreach ( $schema['oneOf'] as &$variant ) {
287 if ( isset( $variant['properties'] ) ) {
288 $variant['properties'] = array_merge(
289 $variant['properties'],
290 $internal['properties']
291 );
292 }
293 }
294 unset( $variant );
295 }
296
297 return $schema;
298 }
299
300 /**
301 * Registers ability categories.
302 *
303 * @since 6.8.0
304 */
305 public function register_categories() {
306 wp_register_ability_category(
307 $this->ability_category(),
308 array(
309 'label' => __( 'SCF Fields', 'secure-custom-fields' ),
310 'description' => __( 'Abilities for managing Secure Custom Fields fields.', 'secure-custom-fields' ),
311 )
312 );
313 }
314
315 /**
316 * Registers all field abilities.
317 *
318 * @since 6.8.0
319 */
320 public function register_abilities() {
321 $this->register_list_ability();
322 $this->register_get_ability();
323 $this->register_create_ability();
324 $this->register_update_ability();
325 $this->register_delete_ability();
326 $this->register_duplicate_ability();
327 $this->register_export_ability();
328 $this->register_import_ability();
329 }
330
331 /**
332 * Registers the list ability.
333 *
334 * @since 6.8.0
335 */
336 private function register_list_ability() {
337 wp_register_ability(
338 $this->ability_name( 'list' ),
339 array(
340 'label' => __( 'List Fields', 'secure-custom-fields' ),
341 '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' ),
342 'category' => $this->ability_category(),
343 'execute_callback' => array( $this, 'list_callback' ),
344 'permission_callback' => 'scf_current_user_has_capability',
345 'meta' => array(
346 'show_in_rest' => true,
347 'mcp' => array( 'public' => true ),
348 'annotations' => array(
349 'readonly' => true,
350 'destructive' => false,
351 'idempotent' => true,
352 ),
353 ),
354 'input_schema' => array(
355 'type' => 'object',
356 'properties' => array(
357 'filter' => array(
358 'type' => 'object',
359 'description' => __( 'Filter fields by parent, type, or name.', 'secure-custom-fields' ),
360 'properties' => array(
361 'parent' => array(
362 'type' => array( 'integer', 'string' ),
363 'description' => __( 'Field group ID or key to filter by.', 'secure-custom-fields' ),
364 ),
365 'type' => array(
366 'type' => 'string',
367 'description' => __( 'Field type to filter by (e.g., text, image).', 'secure-custom-fields' ),
368 ),
369 'name' => array(
370 'type' => 'string',
371 'description' => __( 'Field name to filter by.', 'secure-custom-fields' ),
372 ),
373 ),
374 ),
375 ),
376 ),
377 'output_schema' => array(
378 'type' => 'array',
379 'items' => $this->get_field_with_internal_fields_schema(),
380 ),
381 )
382 );
383 }
384
385 /**
386 * Registers the get ability.
387 *
388 * @since 6.8.0
389 */
390 private function register_get_ability() {
391 wp_register_ability(
392 $this->ability_name( 'get' ),
393 array(
394 'label' => __( 'Get Field', 'secure-custom-fields' ),
395 'description' => __( 'Retrieves a single SCF field by key or ID.', 'secure-custom-fields' ),
396 'category' => $this->ability_category(),
397 'execute_callback' => array( $this, 'get_callback' ),
398 'permission_callback' => 'scf_current_user_has_capability',
399 'meta' => array(
400 'show_in_rest' => true,
401 'mcp' => array( 'public' => true ),
402 'annotations' => array(
403 'readonly' => true,
404 'destructive' => false,
405 'idempotent' => true,
406 ),
407 ),
408 'input_schema' => array(
409 'type' => 'object',
410 'required' => array( 'identifier' ),
411 'properties' => array(
412 'identifier' => $this->get_scf_identifier_schema(),
413 ),
414 ),
415 'output_schema' => $this->get_field_with_internal_fields_schema(),
416 )
417 );
418 }
419
420 /**
421 * Registers the create ability.
422 *
423 * @since 6.8.0
424 */
425 private function register_create_ability() {
426 wp_register_ability(
427 $this->ability_name( 'create' ),
428 array(
429 'label' => __( 'Create Field', 'secure-custom-fields' ),
430 'description' => __( 'Creates a new SCF field with provided configuration. Requires parent (field group ID).', 'secure-custom-fields' ),
431 'category' => $this->ability_category(),
432 'execute_callback' => array( $this, 'create_callback' ),
433 'permission_callback' => 'scf_current_user_has_capability',
434 'meta' => array(
435 'show_in_rest' => true,
436 'mcp' => array( 'public' => true ),
437 'annotations' => array(
438 'readonly' => false,
439 'destructive' => false,
440 'idempotent' => false,
441 ),
442 ),
443 'input_schema' => $this->get_field_schema(),
444 'output_schema' => $this->get_field_with_internal_fields_schema(),
445 )
446 );
447 }
448
449 /**
450 * Registers the update ability.
451 *
452 * @since 6.8.0
453 */
454 private function register_update_ability() {
455 $field_properties = $this->get_field_schema_properties();
456
457 wp_register_ability(
458 $this->ability_name( 'update' ),
459 array(
460 'label' => __( 'Update Field', 'secure-custom-fields' ),
461 'description' => __( 'Updates an existing SCF field. Properties not provided are preserved (merge behavior).', 'secure-custom-fields' ),
462 'category' => $this->ability_category(),
463 'execute_callback' => array( $this, 'update_callback' ),
464 'permission_callback' => 'scf_current_user_has_capability',
465 'meta' => array(
466 'show_in_rest' => true,
467 'mcp' => array( 'public' => true ),
468 'annotations' => array(
469 'readonly' => false,
470 'destructive' => false,
471 'idempotent' => true,
472 ),
473 ),
474 'input_schema' => array(
475 'type' => 'object',
476 'required' => array( 'ID' ),
477 'properties' => array_merge(
478 array(
479 'ID' => array(
480 'type' => 'integer',
481 'description' => __( 'The field ID.', 'secure-custom-fields' ),
482 ),
483 ),
484 $field_properties
485 ),
486 'additionalProperties' => false,
487 ),
488 'output_schema' => $this->get_field_with_internal_fields_schema(),
489 )
490 );
491 }
492
493 /**
494 * Registers the delete ability.
495 *
496 * @since 6.8.0
497 */
498 private function register_delete_ability() {
499 wp_register_ability(
500 $this->ability_name( 'delete' ),
501 array(
502 'label' => __( 'Delete Field', 'secure-custom-fields' ),
503 'description' => __( 'Permanently deletes an SCF field.', 'secure-custom-fields' ),
504 'category' => $this->ability_category(),
505 'execute_callback' => array( $this, 'delete_callback' ),
506 'permission_callback' => 'scf_current_user_has_capability',
507 'meta' => array(
508 'show_in_rest' => true,
509 'mcp' => array( 'public' => true ),
510 'annotations' => array(
511 'readonly' => false,
512 'destructive' => true,
513 'idempotent' => true,
514 ),
515 ),
516 'input_schema' => array(
517 'type' => 'object',
518 'required' => array( 'identifier' ),
519 'properties' => array(
520 'identifier' => $this->get_scf_identifier_schema(),
521 ),
522 ),
523 'output_schema' => array(
524 'type' => 'boolean',
525 ),
526 )
527 );
528 }
529
530 /**
531 * Registers the duplicate ability.
532 *
533 * @since 6.8.0
534 */
535 private function register_duplicate_ability() {
536 wp_register_ability(
537 $this->ability_name( 'duplicate' ),
538 array(
539 'label' => __( 'Duplicate Field', 'secure-custom-fields' ),
540 'description' => __( 'Creates a copy of an SCF field with a new unique key. Optionally specify a new parent field group.', 'secure-custom-fields' ),
541 'category' => $this->ability_category(),
542 'execute_callback' => array( $this, 'duplicate_callback' ),
543 'permission_callback' => 'scf_current_user_has_capability',
544 'meta' => array(
545 'show_in_rest' => true,
546 'mcp' => array( 'public' => true ),
547 'annotations' => array(
548 'readonly' => false,
549 'destructive' => false,
550 'idempotent' => false,
551 ),
552 ),
553 'input_schema' => array(
554 'type' => 'object',
555 'required' => array( 'identifier' ),
556 'properties' => array(
557 'identifier' => $this->get_scf_identifier_schema(),
558 'new_parent_id' => array(
559 'type' => 'integer',
560 'description' => __( 'Optional field group ID to place the duplicate in.', 'secure-custom-fields' ),
561 ),
562 ),
563 ),
564 'output_schema' => $this->get_field_with_internal_fields_schema(),
565 )
566 );
567 }
568
569 /**
570 * Registers the export ability.
571 *
572 * @since 6.8.0
573 */
574 private function register_export_ability() {
575 wp_register_ability(
576 $this->ability_name( 'export' ),
577 array(
578 'label' => __( 'Export Field', 'secure-custom-fields' ),
579 'description' => __( 'Exports an SCF field as JSON for backup or transfer. Internal fields (ID, local, _valid) are stripped.', 'secure-custom-fields' ),
580 'category' => $this->ability_category(),
581 'execute_callback' => array( $this, 'export_callback' ),
582 'permission_callback' => 'scf_current_user_has_capability',
583 'meta' => array(
584 'show_in_rest' => true,
585 'mcp' => array( 'public' => true ),
586 'annotations' => array(
587 'readonly' => true,
588 'destructive' => false,
589 'idempotent' => true,
590 ),
591 ),
592 'input_schema' => array(
593 'type' => 'object',
594 'required' => array( 'identifier' ),
595 'properties' => array(
596 'identifier' => $this->get_scf_identifier_schema(),
597 ),
598 ),
599 'output_schema' => $this->get_field_schema(),
600 )
601 );
602 }
603
604 /**
605 * Registers the import ability.
606 *
607 * @since 6.8.0
608 */
609 private function register_import_ability() {
610 wp_register_ability(
611 $this->ability_name( 'import' ),
612 array(
613 'label' => __( 'Import Field', 'secure-custom-fields' ),
614 'description' => __( 'Imports an SCF field from JSON data. If key exists, updates existing; otherwise creates new.', 'secure-custom-fields' ),
615 'category' => $this->ability_category(),
616 'execute_callback' => array( $this, 'import_callback' ),
617 'permission_callback' => 'scf_current_user_has_capability',
618 'meta' => array(
619 'show_in_rest' => true,
620 'mcp' => array( 'public' => true ),
621 'annotations' => array(
622 'readonly' => false,
623 'destructive' => false,
624 'idempotent' => true,
625 ),
626 ),
627 'input_schema' => $this->get_field_schema(),
628 'output_schema' => $this->get_field_with_internal_fields_schema(),
629 )
630 );
631 }
632
633 /**
634 * Handles the list ability callback.
635 *
636 * @since 6.8.0
637 *
638 * @param array $input The input parameters.
639 * @return array List of fields.
640 */
641 public function list_callback( $input ) {
642 $filter = isset( $input['filter'] ) && is_array( $input['filter'] ) ? $input['filter'] : array();
643 return $this->manager()->filter_posts( $this->manager()->get_posts(), $filter );
644 }
645
646 /**
647 * Handles the get ability callback.
648 *
649 * @since 6.8.0
650 *
651 * @param array $input The input parameters.
652 * @return array|WP_Error Field data or error if not found.
653 */
654 public function get_callback( $input ) {
655 $field = $this->manager()->get_post( $input['identifier'] );
656 if ( ! $field ) {
657 return $this->not_found_error();
658 }
659 return $field;
660 }
661
662 /**
663 * Handles the create ability callback.
664 *
665 * @since 6.8.0
666 *
667 * @param array $input The field data to create.
668 * @return array|WP_Error Created field or error on failure.
669 */
670 public function create_callback( $input ) {
671 // Check for existing field with same key.
672 if ( isset( $input['key'] ) && $this->manager()->get_post( $input['key'] ) ) {
673 return new WP_Error(
674 'already_exists',
675 __( 'Field with this key already exists.', 'secure-custom-fields' )
676 );
677 }
678
679 if ( ! $this->parent_exists( $input['parent'] ) ) {
680 return new WP_Error(
681 'parent_not_found',
682 __( 'Parent field group or field does not exist.', 'secure-custom-fields' ),
683 array( 'status' => 400 )
684 );
685 }
686
687 $field = $this->manager()->update_post( $input );
688 if ( ! $field ) {
689 return new WP_Error(
690 'create_failed',
691 __( 'Failed to create field.', 'secure-custom-fields' )
692 );
693 }
694 return $field;
695 }
696
697 /**
698 * Handles the update ability callback.
699 *
700 * @since 6.8.0
701 *
702 * @param array $input The field data to update.
703 * @return array|WP_Error Updated field or error on failure.
704 */
705 public function update_callback( $input ) {
706 $existing = $this->manager()->get_post( $input['ID'] );
707 if ( ! $existing ) {
708 return $this->not_found_error();
709 }
710
711 // Merge with existing data.
712 $updated_data = array_merge( $existing, $input );
713 $field = $this->manager()->update_post( $updated_data );
714
715 if ( ! $field ) {
716 return new WP_Error(
717 'update_failed',
718 __( 'Failed to update field.', 'secure-custom-fields' )
719 );
720 }
721 return $field;
722 }
723
724 /**
725 * Handles the delete ability callback.
726 *
727 * @since 6.8.0
728 *
729 * @param array $input The input parameters.
730 * @return bool|WP_Error True on success or error on failure.
731 */
732 public function delete_callback( $input ) {
733 if ( ! $this->manager()->get_post( $input['identifier'] ) ) {
734 return $this->not_found_error();
735 }
736
737 if ( ! $this->manager()->delete_post( $input['identifier'] ) ) {
738 return new WP_Error(
739 'delete_failed',
740 __( 'Failed to delete field.', 'secure-custom-fields' )
741 );
742 }
743 return true;
744 }
745
746 /**
747 * Handles the duplicate ability callback.
748 *
749 * @since 6.8.0
750 *
751 * @param array $input The input parameters.
752 * @return array|WP_Error Duplicated field or error on failure.
753 */
754 public function duplicate_callback( $input ) {
755 if ( ! $this->manager()->get_post( $input['identifier'] ) ) {
756 return $this->not_found_error();
757 }
758
759 $new_parent_id = isset( $input['new_parent_id'] ) ? $input['new_parent_id'] : 0;
760
761 // Validate that new_parent_id references an existing parent (field group or parent field).
762 if ( $new_parent_id && ! $this->parent_exists( $new_parent_id ) ) {
763 return new WP_Error(
764 'invalid_new_parent_id',
765 sprintf(
766 /* translators: %d: Invalid parent ID */
767 __( 'Invalid new_parent_id: %d is not a valid field group or parent field.', 'secure-custom-fields' ),
768 $new_parent_id
769 ),
770 array( 'status' => 400 )
771 );
772 }
773
774 $duplicated = $this->manager()->duplicate_post( $input['identifier'], $new_parent_id );
775
776 if ( ! $duplicated ) {
777 return new WP_Error(
778 'duplicate_failed',
779 __( 'Failed to duplicate field.', 'secure-custom-fields' )
780 );
781 }
782 return $duplicated;
783 }
784
785 /**
786 * Handles the export ability callback.
787 *
788 * @since 6.8.0
789 *
790 * @param array $input The input parameters.
791 * @return array|WP_Error Exported field data or error on failure.
792 */
793 public function export_callback( $input ) {
794 $field = $this->manager()->get_post( $input['identifier'] );
795 if ( ! $field ) {
796 return $this->not_found_error();
797 }
798
799 $export = $this->manager()->prepare_post_for_export( $field );
800 if ( ! $export ) {
801 return new WP_Error(
802 'export_failed',
803 __( 'Failed to export field.', 'secure-custom-fields' )
804 );
805 }
806 return $export;
807 }
808
809 /**
810 * Handles the import ability callback.
811 *
812 * @since 6.8.0
813 *
814 * @param array|object $input The field data to import.
815 * @return array|WP_Error Imported field or error on failure.
816 */
817 public function import_callback( $input ) {
818 if ( ! $this->parent_exists( $input['parent'] ) ) {
819 return new WP_Error(
820 'parent_not_found',
821 __( 'Parent field group or field does not exist.', 'secure-custom-fields' ),
822 array( 'status' => 400 )
823 );
824 }
825
826 $imported = $this->manager()->import_post( $input );
827 if ( ! $imported ) {
828 return new WP_Error(
829 'import_failed',
830 __( 'Failed to import field.', 'secure-custom-fields' )
831 );
832 }
833 return $imported;
834 }
835
836 /**
837 * Checks if the parent field group or field exists.
838 *
839 * @since 6.8.0
840 *
841 * @param int|string $parent_id The parent ID or key.
842 * @return bool True if parent exists, false otherwise.
843 */
844 private function parent_exists( $parent_id ) {
845 /**
846 * Filters the result of the parent existence check.
847 *
848 * @since 6.8.0
849 *
850 * @param bool|null $exists The existence result. Null to use default logic.
851 * @param int|string $parent_id The parent ID or key being checked.
852 */
853 $filtered = apply_filters( 'scf_field_parent_exists', null, $parent_id );
854 if ( null !== $filtered ) {
855 return (bool) $filtered;
856 }
857
858 // Parent can be a field group or a parent field (for sub-fields).
859 return (bool) acf_get_field_group( $parent_id ) || (bool) acf_get_field( $parent_id );
860 }
861
862 /**
863 * Creates a not found error response.
864 *
865 * @since 6.8.0
866 *
867 * @return WP_Error
868 */
869 private function not_found_error() {
870 return new WP_Error(
871 'not_found',
872 __( 'Field not found.', 'secure-custom-fields' ),
873 array( 'status' => 404 )
874 );
875 }
876 }
877
878 // Initialize abilities instance.
879 acf_new_instance( 'SCF_Field_Abilities' );
880
881 endif; // class_exists check.
882