PluginProbe ʕ •ᴥ•ʔ
Secure Custom Fields / trunk
Secure Custom Fields vtrunk
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 / src / AI / Abilities / PostType.php
secure-custom-fields / src / AI / Abilities Last commit date
Abilities.php 2 months ago AbstractAbilityGroup.php 2 months ago FieldGroup.php 1 month ago PostType.php 2 months ago SCF_REST_Ability.php 2 months ago Taxonomy.php 1 month ago
PostType.php
756 lines
1 <?php
2 /**
3 * ACF 6.8.0 feature port.
4 *
5 * @package wordpress/secure-custom-fields
6 */
7
8 // phpcs:disable -- Upstream ACF 6.8.0 feature-port files are kept close to source.
9
10 namespace SCF\AI\Abilities;
11
12 use WP_Error;
13
14 // Exit if accessed directly.
15 defined( 'ABSPATH' ) || exit;
16
17 /**
18 * ACF Post Type Abilities
19 *
20 * Handles ACF custom post type related abilities for the WordPress Abilities API.
21 */
22 class PostType extends AbstractAbilityGroup {
23
24 /**
25 * Register post type related abilities.
26 *
27 * @since 6.8.0
28 *
29 * @return void
30 */
31 public function register_abilities() {
32 if ( ! $this->is_abilities_api_available() ) {
33 return;
34 }
35
36 // Register ACF Custom Post Types resource.
37 $this->register_ability(
38 'acf/custom-post-types',
39 array(
40 'label' => __( 'SCF Custom Post Types', 'secure-custom-fields' ),
41 'description' => __( 'Get all SCF registered custom post types', 'secure-custom-fields' ),
42 'category' => 'acf-field-management',
43 'input_schema' => array(
44 'type' => array( 'object', 'null' ),
45 'properties' => array(),
46 'additionalProperties' => false,
47 ),
48 'output_schema' => array(
49 'type' => 'object',
50 'properties' => array(
51 'custom_post_types' => array(
52 'type' => 'array',
53 'items' => array( 'type' => 'object' ),
54 ),
55 'count' => array( 'type' => 'integer' ),
56 'message' => array( 'type' => 'string' ),
57 ),
58 ),
59 'execute_callback' => array( $this, 'get_custom_post_types' ),
60 'permission_callback' => function () {
61 return current_user_can( acf_get_setting( 'capability' ) );
62 },
63 'meta' => array(
64 'annotations' => array(
65 'readonly' => true,
66 'destructive' => false,
67 'idempotent' => true,
68 ),
69 'show_in_rest' => true,
70 ),
71 )
72 );
73
74 // Register custom post type ability.
75 $this->register_ability(
76 'acf/register-custom-post-type',
77 array(
78 'label' => __( 'Register Custom Post Type', 'secure-custom-fields' ),
79 'description' => __( 'Register a new post type definition in WordPress (e.g., "Book", "Event"). This creates the post type schema itself, not individual posts. Use the create post abilities to add posts to an existing post type.', 'secure-custom-fields' ),
80 'category' => 'acf-field-management',
81 'input_schema' => array(
82 'type' => 'object',
83 'properties' => array(
84 'post_type' => array(
85 'type' => 'string',
86 'pattern' => '^[a-z0-9_-]*$',
87 'maxLength' => 20,
88 'description' => 'The post type key (slug)',
89 'required' => true,
90 ),
91 'label' => array(
92 'type' => 'string',
93 'description' => 'The singular label for the post type',
94 'required' => true,
95 ),
96 'plural_label' => array(
97 'type' => 'string',
98 'description' => 'The plural label for the post type',
99 'required' => true,
100 ),
101 'description' => array(
102 'type' => 'string',
103 'description' => 'Description of the post type',
104 'required' => false,
105 ),
106 'public' => array(
107 'type' => 'boolean',
108 'description' => 'Whether the post type is public',
109 'required' => false,
110 ),
111 'hierarchical' => array(
112 'type' => 'boolean',
113 'description' => 'Whether the post type is hierarchical',
114 'required' => false,
115 ),
116 'supports' => array(
117 'type' => 'array',
118 'description' => 'Features the post type supports. Can be array of strings ["title", "editor"] or object {"title": true, "editor": false}. Available: title, editor, author, thumbnail, excerpt, comments, trackbacks, revisions, page-attributes, custom-fields, post-formats',
119 'required' => false,
120 ),
121 'show_in_rest' => array(
122 'type' => 'boolean',
123 'description' => 'Whether to show this post type in the REST API (required for AI abilities)',
124 'required' => false,
125 ),
126 'rest_base' => array(
127 'type' => 'string',
128 'description' => 'Custom REST API base path (defaults to post type key)',
129 'required' => false,
130 ),
131 'allow_ai_access' => array(
132 'type' => 'boolean',
133 'description' => 'Whether to allow AI access to this post type',
134 'required' => false,
135 ),
136 'ai_description' => array(
137 'type' => 'string',
138 'description' => 'Description to help AI understand the purpose of this post type',
139 'required' => false,
140 ),
141 'menu_icon' => array(
142 'type' => array( 'string', 'object' ),
143 'description' => 'Menu icon (dashicon class, URL, or object with type and value)',
144 'required' => false,
145 ),
146 'menu_position' => array(
147 'type' => 'integer',
148 'description' => 'Position in the admin menu (5-100)',
149 'required' => false,
150 ),
151 'has_archive' => array(
152 'type' => 'boolean',
153 'description' => 'Whether the post type has an archive page',
154 'required' => false,
155 ),
156 'has_archive_slug' => array(
157 'type' => 'string',
158 'description' => 'Custom slug for the archive page',
159 'required' => false,
160 ),
161 'taxonomies' => array(
162 'type' => 'array',
163 'description' => 'Array of taxonomy names to associate with this post type',
164 'items' => array( 'type' => 'string' ),
165 'required' => false,
166 ),
167 ),
168 ),
169 'output_schema' => array(
170 'type' => 'object',
171 'properties' => array(
172 'success' => array( 'type' => 'boolean' ),
173 'post_type' => array( 'type' => 'object' ),
174 'message' => array( 'type' => 'string' ),
175 ),
176 ),
177 'execute_callback' => array( $this, 'create_custom_post_type' ),
178 'permission_callback' => function () {
179 return current_user_can( acf_get_setting( 'capability' ) );
180 },
181 'meta' => array(
182 'annotations' => array(
183 'readonly' => false,
184 'destructive' => false,
185 'idempotent' => false,
186 ),
187 'show_in_rest' => true,
188 ),
189 )
190 );
191
192 // Register abilities for each ACF custom post type that has REST API enabled.
193 $this->register_acf_post_type_abilities();
194 }
195
196 /**
197 * Register abilities for each ACF custom post type that has REST API enabled.
198 *
199 * @since 6.8.0
200 *
201 * @return void
202 */
203 private function register_acf_post_type_abilities() {
204 $acf_post_types = acf_get_acf_post_types();
205
206 foreach ( $acf_post_types as $acf_post_type ) {
207 $post_type_name = $acf_post_type['post_type'] ?? '';
208 if ( ! $post_type_name ) {
209 continue;
210 }
211
212 // Check if the post type is active and AI access is enabled.
213 if ( empty( $acf_post_type['active'] ) || empty( $acf_post_type['allow_ai_access'] ) ) {
214 continue;
215 }
216
217 // Sanitize post type name for feature ID (convert underscores to hyphens, ensure lowercase)
218 $sanitized_post_type_name = str_replace( '_', '-', strtolower( $post_type_name ) );
219
220 // Skip if we can't retrieve the post type object or if it isn't configured with REST API access.
221 $post_type_object = get_post_type_object( $post_type_name );
222 if ( ! $post_type_object || empty( $post_type_object->show_in_rest ) ) {
223 continue;
224 }
225
226 $rest_base = acf_get_object_type_rest_base( $post_type_object );
227 $post_type_label = $post_type_object->labels->singular_name ?? $post_type_name;
228 $post_type_label_plural = $post_type_object->labels->name ?? $post_type_name . 's';
229
230 // Get AI description for enhanced ability descriptions
231 $ai_description = $acf_post_type['ai_description'] ?? '';
232 $description_suffix = $ai_description ? ' ' . $ai_description : '';
233
234 // Get ACF fields for this post type.
235 $acf_fields = $this->get_acf_fields_for_object( 'post_type', $post_type_name );
236
237 // Get schemas from REST controller.
238 $item_schema = $this->get_rest_item_output_schema( $acf_fields, $post_type_name );
239 $collection_schema = $this->get_rest_item_output_schema( $acf_fields, $post_type_name, 'collection' );
240
241 // Register query/list feature for this post type
242 $this->register_ability(
243 'acf/' . $sanitized_post_type_name . 's',
244 array(
245 /* translators: %s The plural label for the custom post type. */
246 'label' => sprintf( __( 'Query %s', 'secure-custom-fields' ), $post_type_label_plural ),
247 /* translators: %s The plural label for the custom post type. */
248 'description' => sprintf( __( 'Get a list of %s that match the query parameters.', 'secure-custom-fields' ), strtolower( $post_type_label_plural ) ) . $description_suffix,
249 'category' => 'wordpress-content-discovery',
250 'input_schema' => array(
251 'type' => array( 'object', 'null' ),
252 'properties' => array(
253 'per_page' => array(
254 'type' => 'integer',
255 'default' => 10,
256 'minimum' => 1,
257 'maximum' => 100,
258 ),
259 'page' => array(
260 'type' => 'integer',
261 'default' => 1,
262 'minimum' => 1,
263 ),
264 'search' => array(
265 'type' => 'string',
266 'description' => 'Limit results to those matching a string.',
267 ),
268 'slug' => array(
269 'type' => 'array',
270 'items' => array(
271 'type' => 'string',
272 ),
273 'description' => 'Limit result set to posts with one or more specific slugs.',
274 ),
275 'orderby' => array(
276 'type' => 'string',
277 'enum' => array( 'date', 'id', 'modified', 'relevance', 'slug', 'title' ),
278 'default' => 'date',
279 'description' => 'Sort collection by post attribute.',
280 ),
281 'order' => array(
282 'type' => 'string',
283 'enum' => array( 'asc', 'desc' ),
284 'default' => 'desc',
285 'description' => 'Order sort attribute ascending or descending.',
286 ),
287 ),
288 'additionalProperties' => false,
289 ),
290 'output_schema' => $collection_schema,
291 'execute_callback' => function ( $input = array() ) use ( $rest_base ) {
292 return $this->execute_rest_request( 'GET', $rest_base, $input );
293 },
294 'permission_callback' => function () use ( $post_type_object ) {
295 // For querying, allow if user can read or if they can read private posts of this type.
296 return current_user_can( 'read' ) || current_user_can( $post_type_object->cap->read_private_posts );
297 },
298 'meta' => array(
299 'annotations' => array(
300 'readonly' => true,
301 'destructive' => false,
302 'idempotent' => true,
303 ),
304 'show_in_rest' => true,
305 ),
306 'ability_class' => self::REST_ABILITY_CLASS,
307 )
308 );
309
310 // Register create ability for this post type
311 $this->register_ability(
312 'acf/create-' . $sanitized_post_type_name,
313 array(
314 /* translators: %s The singular label for the custom post type. */
315 'label' => sprintf( __( 'Create %s', 'secure-custom-fields' ), $post_type_label ),
316 /* translators: %s The singular label for the custom post type. */
317 'description' => sprintf( __( 'Create a new "%s" post item.', 'secure-custom-fields' ), strtolower( $post_type_label ) ) . $description_suffix,
318 'category' => 'wordpress-content-discovery',
319 'input_schema' => $this->get_rest_item_input_schema( $acf_fields, $post_type_label, 'create', $post_type_name ),
320 'output_schema' => $item_schema,
321 'execute_callback' => function ( $input = array() ) use ( $rest_base ) {
322 return $this->execute_rest_request( 'POST', $rest_base, $input );
323 },
324 'permission_callback' => function () use ( $post_type_object ) {
325 return current_user_can( $post_type_object->cap->create_posts );
326 },
327 'meta' => array(
328 'annotations' => array(
329 'readonly' => false,
330 'destructive' => false,
331 'idempotent' => false,
332 ),
333 'show_in_rest' => true,
334 ),
335 'ability_class' => self::REST_ABILITY_CLASS,
336 )
337 );
338
339 // Register view single ability for this post type
340 $this->register_ability(
341 'acf/view-' . $sanitized_post_type_name,
342 array(
343 /* translators: %s The singular label for the custom post type. */
344 'label' => sprintf( __( 'View a %s', 'secure-custom-fields' ), $post_type_label ),
345 /* translators: %s The singular label for the custom post type. */
346 'description' => sprintf( __( 'Get a %s by its ID.', 'secure-custom-fields' ), strtolower( $post_type_label ) ) . $description_suffix,
347 'category' => 'wordpress-content-discovery',
348 'input_schema' => array(
349 'type' => 'object',
350 'properties' => array(
351 'id' => array(
352 'type' => 'integer',
353 'description' => sprintf( 'The ID of the %s to view.', strtolower( $post_type_label ) ),
354 'required' => true,
355 ),
356 ),
357 ),
358 'output_schema' => $item_schema,
359 'execute_callback' => function ( $input = array() ) use ( $rest_base ) {
360 $item_id = $input['id'] ?? null;
361 return $this->execute_rest_request( 'GET', $rest_base, $input, $item_id );
362 },
363 'permission_callback' => function () {
364 return current_user_can( 'read' );
365 },
366 'meta' => array(
367 'annotations' => array(
368 'readonly' => true,
369 'destructive' => false,
370 'idempotent' => true,
371 ),
372 'show_in_rest' => true,
373 ),
374 'ability_class' => self::REST_ABILITY_CLASS,
375 )
376 );
377
378 // Register update ability for this post type
379 $this->register_ability(
380 'acf/update-' . $sanitized_post_type_name,
381 array(
382 /* translators: %s The singular label for the custom post type. */
383 'label' => sprintf( __( 'Update a %s', 'secure-custom-fields' ), $post_type_label ),
384 /* translators: %s The singular label for the custom post type. */
385 'description' => sprintf( __( 'Update a %s by its ID.', 'secure-custom-fields' ), strtolower( $post_type_label ) ) . $description_suffix,
386 'category' => 'wordpress-content-discovery',
387 'input_schema' => $this->get_rest_item_input_schema( $acf_fields, $post_type_label, 'update', $post_type_name ),
388 'output_schema' => $item_schema,
389 'execute_callback' => function ( $input = array() ) use ( $rest_base ) {
390 $item_id = $input['id'] ?? null;
391 return $this->execute_rest_request( 'PUT', $rest_base, $input, $item_id );
392 },
393 'permission_callback' => function () use ( $post_type_object ) {
394 return current_user_can( $post_type_object->cap->edit_posts );
395 },
396 'meta' => array(
397 'annotations' => array(
398 'readonly' => false,
399 'destructive' => false,
400 'idempotent' => true,
401 ),
402 'show_in_rest' => true,
403 ),
404 'ability_class' => self::REST_ABILITY_CLASS,
405 )
406 );
407
408 // Register delete ability for this post type.
409 $this->register_ability(
410 'acf/delete-' . $sanitized_post_type_name,
411 array(
412 /* translators: %s The singular label for the custom post type. */
413 'label' => sprintf( __( 'Delete a %s', 'secure-custom-fields' ), $post_type_label ),
414 /* translators: %s The singular label for the custom post type. */
415 'description' => sprintf( __( 'Delete a %s by its ID.', 'secure-custom-fields' ), strtolower( $post_type_label ) ) . $description_suffix,
416 'category' => 'wordpress-content-discovery',
417 'input_schema' => array(
418 'type' => 'object',
419 'properties' => array(
420 'id' => array(
421 'type' => 'integer',
422 'description' => sprintf( 'The ID of the %s to delete.', strtolower( $post_type_label ) ),
423 'required' => true,
424 ),
425 ),
426 ),
427 'output_schema' => $item_schema,
428 'execute_callback' => function ( $input = array() ) use ( $rest_base ) {
429 $item_id = $input['id'] ?? null;
430 return $this->execute_rest_request( 'DELETE', $rest_base, $input, $item_id );
431 },
432 'permission_callback' => function () use ( $post_type_object ) {
433 return current_user_can( $post_type_object->cap->delete_posts );
434 },
435 'meta' => array(
436 'annotations' => array(
437 'readonly' => false,
438 'destructive' => true,
439 'idempotent' => true,
440 ),
441 'show_in_rest' => true,
442 ),
443 'ability_class' => self::REST_ABILITY_CLASS,
444 )
445 );
446 }
447 }
448
449 /**
450 * Builds a basic input schema for creating or updating a post type item.
451 *
452 * We only include a few common properties since the REST API handles the
453 * validation and updates.
454 *
455 * @since 6.8.0
456 *
457 * @param array $acf_fields An array of ACF fields present on the post type.
458 * @param string $post_type_label The singular label for the post type.
459 * @param string $action The action being performed on the item.
460 * @param string $post_type_name The post type name/key.
461 * @return array
462 */
463 private function get_rest_item_input_schema( array $acf_fields, string $post_type_label, string $action = 'create', string $post_type_name = '' ): array {
464 $schema = array(
465 'type' => 'object',
466 'properties' => array(),
467 );
468
469 if ( 'update' === $action ) {
470 $schema['properties']['id'] = array(
471 'type' => 'integer',
472 'description' => sprintf( 'The ID of the %s to update.', strtolower( $post_type_label ) ),
473 'required' => true,
474 );
475 }
476
477 $schema['properties']['title'] = array(
478 'type' => 'string',
479 'description' => sprintf( 'The title of the %s.', strtolower( $post_type_label ) ),
480 'required' => 'update' !== $action,
481 );
482
483 $schema['properties']['content'] = array(
484 'type' => 'string',
485 'description' => sprintf( 'The content of the %s.', strtolower( $post_type_label ) ),
486 'required' => false,
487 );
488
489 // TODO: Provide enum?
490 $schema['properties']['status'] = array(
491 'type' => 'string',
492 'description' => 'The status of the post (publish, draft, etc.)',
493 'required' => false,
494 );
495
496 // Only add featured_media if the post type supports thumbnails.
497 if ( ! empty( $post_type_name ) && post_type_supports( $post_type_name, 'thumbnail' ) ) {
498 $schema['properties']['featured_media'] = array(
499 'type' => 'integer',
500 'description' => 'The ID of the featured image (attachment) for this post. The attachment must exist and be an image. Set to 0 to remove; invalid or non-image IDs will not set a featured image.',
501 'required' => false,
502 );
503 }
504
505 // Only add author if the post type supports author.
506 if ( ! empty( $post_type_name ) && post_type_supports( $post_type_name, 'author' ) ) {
507 $schema['properties']['author'] = array(
508 'type' => 'integer',
509 'description' => 'The ID of the user to assign as the author of this post. The user must exist and have permission to be assigned as an author.',
510 'required' => false,
511 );
512 }
513
514 return $this->add_acf_fields_to_schema( $schema, $acf_fields );
515 }
516
517 /**
518 * Gets the REST schema for item(s) in a CPT.
519 *
520 * @since 6.8.0
521 *
522 * @param array $acf_fields An array of ACF fields present on the post type.
523 * @param string $post_type The post type to get the schema for.
524 * @param string $type Schema type: 'item' or 'collection'.
525 * @return array|null Schema array or null if not available.
526 */
527 private function get_rest_item_output_schema( array $acf_fields, string $post_type, string $type = 'item' ) {
528 $post_type_object = get_post_type_object( $post_type );
529
530 if ( ! $post_type_object ) {
531 return null;
532 }
533
534 $controller = $post_type_object->get_rest_controller();
535 if ( ! $controller || ! method_exists( $controller, 'get_public_item_schema' ) ) {
536 return null;
537 }
538
539 $schema = $controller->get_public_item_schema();
540 $schema = $this->add_acf_fields_to_schema( $schema, $acf_fields );
541
542 if ( $type === 'collection' ) {
543 return array(
544 'type' => 'array',
545 'items' => $schema,
546 );
547 }
548
549 return $schema;
550 }
551
552 /**
553 * Callback for the "acf/get-custom-post-types" ability.
554 *
555 * @since 6.8.0
556 *
557 * @param array $input An array of input args.
558 * @return array
559 */
560 public function get_custom_post_types( $input ) {
561 unset( $input ); // Not used, but required by interface.
562
563 // Get ACF custom post types.
564 $acf_post_types = acf_get_acf_post_types();
565 $custom_post_types = array();
566
567 foreach ( $acf_post_types as $acf_post_type ) {
568 $post_type_name = $acf_post_type['post_type'] ?? '';
569 if ( ! $post_type_name ) {
570 continue;
571 }
572
573 if ( empty( $acf_post_type['active'] ) || empty( $acf_post_type['allow_ai_access'] ) ) {
574 continue;
575 }
576
577 $post_type_object = get_post_type_object( $post_type_name );
578 if ( $post_type_object ) {
579 $post_type_data = array(
580 'post_type' => $post_type_name,
581 'label' => $post_type_object->label,
582 'labels' => (array) $post_type_object->labels,
583 'description' => $post_type_object->description,
584 'public' => $post_type_object->public,
585 'hierarchical' => $post_type_object->hierarchical,
586 'supports' => get_all_post_type_supports( $post_type_name ),
587 'acf_settings' => $acf_post_type,
588 );
589
590 // Add ACF field groups information
591 $acf_fields = $this->get_acf_fields_for_object( 'post_type', $post_type_name );
592 if ( ! empty( $acf_fields ) ) {
593 $post_type_data['acf_field_groups'] = $acf_fields;
594 }
595
596 $custom_post_types[] = $post_type_data;
597 }
598 }
599
600 $count = count( $custom_post_types );
601
602 return array(
603 'custom_post_types' => $custom_post_types,
604 'count' => $count,
605 'message' => sprintf(
606 /* translators: %d: Number of SCF custom post types */
607 _n( 'Found %d SCF custom post type', 'Found %d SCF custom post types', $count, 'secure-custom-fields' ),
608 $count
609 ),
610 );
611 }
612
613 /**
614 * Callback for the "acf/register-custom-post-type" ability.
615 *
616 * @since 6.8.0
617 *
618 * @param array $input An array of input args.
619 * @return array|WP_Error
620 */
621 public function create_custom_post_type( $input ) {
622 // Required parameters
623 $post_type = sanitize_key( $input['post_type'] );
624 $label = sanitize_text_field( $input['label'] );
625 $plural_label = sanitize_text_field( $input['plural_label'] );
626
627 // Check if post type already exists.
628 if ( post_type_exists( $post_type ) ) {
629 return new WP_Error(
630 'post_type_exists',
631 __( 'A post type with this key already exists', 'secure-custom-fields' ),
632 array( 'status' => 400 )
633 );
634 }
635
636 // Basic optional parameters
637 $description = sanitize_text_field( $input['description'] ?? '' );
638 $public = $input['public'] ?? true;
639 $hierarchical = $input['hierarchical'] ?? false;
640 $supports = $input['supports'] ?? array( 'title', 'editor' );
641
642 // REST API settings
643 $show_in_rest = $input['show_in_rest'] ?? true;
644 $rest_base = $input['rest_base'] ?? '';
645
646 // AI settings
647 $allow_ai_access = $input['allow_ai_access'] ?? true;
648 $ai_description = $input['ai_description'] ?? '';
649
650 // Menu settings
651 $menu_icon = $input['menu_icon'] ?? '';
652 $menu_position = $input['menu_position'] ?? null;
653
654 // Archive settings
655 $has_archive = $input['has_archive'] ?? false;
656 $has_archive_slug = $input['has_archive_slug'] ?? '';
657
658 // Taxonomies
659 $taxonomies = $input['taxonomies'] ?? array();
660
661 // Handle supports parameter - convert from associative array to simple array if needed
662 if ( is_array( $supports ) && ! empty( $supports ) ) {
663 // Check if this is an associative array (like {'title': true, 'editor': false})
664 if ( array_keys( $supports ) !== range( 0, count( $supports ) - 1 ) ) {
665 // Convert associative array to simple array of enabled features
666 $enabled_supports = array();
667 foreach ( $supports as $feature => $enabled ) {
668 if ( $enabled ) {
669 $enabled_supports[] = sanitize_key( $feature );
670 }
671 }
672 $supports = $enabled_supports;
673 } else {
674 $supports = array_map( 'sanitize_key', $supports );
675 }
676 }
677
678 // Use ACF's method to create the post type.
679 $post_type_data = array(
680 'key' => uniqid( 'post_type_' ),
681 'post_type' => $post_type,
682 'title' => $plural_label,
683 'labels' => wp_parse_args(
684 array(
685 'name' => $plural_label,
686 'singular_name' => $label,
687 ),
688 acf_get_internal_post_type_instance( 'acf-post-type' )->get_settings_array()['labels']
689 ),
690 'description' => $description,
691 'public' => $public ? 1 : 0,
692 'hierarchical' => $hierarchical ? 1 : 0,
693 'supports' => $supports,
694 'active' => 1,
695 // REST API settings
696 'show_in_rest' => $show_in_rest ? 1 : 0,
697 // AI settings
698 'allow_ai_access' => $allow_ai_access ? 1 : 0,
699 );
700
701 // Add optional settings only if provided
702 if ( ! empty( $rest_base ) ) {
703 $post_type_data['rest_base'] = sanitize_text_field( $rest_base );
704 }
705
706 if ( ! empty( $ai_description ) ) {
707 $post_type_data['ai_description'] = sanitize_text_field( $ai_description );
708 }
709
710 if ( ! empty( $menu_icon ) ) {
711 // Handle menu_icon which can be string or object
712 if ( is_string( $menu_icon ) ) {
713 $post_type_data['menu_icon'] = array(
714 'type' => strpos( $menu_icon, 'http' ) === 0 ? 'url' : 'dashicons',
715 'value' => sanitize_text_field( $menu_icon ),
716 );
717 } elseif ( is_array( $menu_icon ) && isset( $menu_icon['type'], $menu_icon['value'] ) ) {
718 $post_type_data['menu_icon'] = array(
719 'type' => sanitize_text_field( $menu_icon['type'] ),
720 'value' => sanitize_text_field( $menu_icon['value'] ),
721 );
722 }
723 }
724
725 if ( ! is_null( $menu_position ) ) {
726 $post_type_data['menu_position'] = intval( $menu_position );
727 }
728
729 if ( $has_archive ) {
730 $post_type_data['has_archive'] = 1;
731 if ( ! empty( $has_archive_slug ) ) {
732 $post_type_data['has_archive_slug'] = sanitize_title( $has_archive_slug );
733 }
734 }
735
736 if ( ! empty( $taxonomies ) && is_array( $taxonomies ) ) {
737 $post_type_data['taxonomies'] = array_map( 'sanitize_key', $taxonomies );
738 }
739
740 $result = acf_import_post_type( $post_type_data );
741
742 if ( empty( $result['ID'] ) || ! is_int( $result['ID'] ) || ! post_type_exists( $result['post_type'] ) ) {
743 return new WP_Error(
744 'post_type_creation_failed',
745 __( 'Failed to create the custom post type', 'secure-custom-fields' )
746 );
747 }
748
749 return array(
750 'success' => true,
751 'post_type' => $result,
752 'message' => __( 'SCF custom post type created successfully', 'secure-custom-fields' ),
753 );
754 }
755 }
756