PluginProbe ʕ •ᴥ•ʔ
Advanced Custom Fields (ACF®) / 6.8.9
Advanced Custom Fields (ACF®) v6.8.9
6.8.9 6.8.8 6.8.7 6.8.6 6.8.5 6.8.4 6.8.3 6.8.2 6.8.1 5.8.5 5.8.6 5.8.7 5.8.8 5.8.9 5.9.0 5.9.1 5.9.2 5.9.3 5.9.4 5.9.5 5.9.6 5.9.7 5.9.8 5.9.9 6.0.0 6.0.1 6.0.2 6.0.3 6.0.4 6.0.5 6.0.6 6.0.7 6.1.0 6.1.1 6.1.2 6.1.3 6.1.4 6.1.5 6.1.6 6.1.7 6.1.8 6.2.0 6.2.1 6.2.2 6.2.3 6.2.4 6.2.5 6.2.6 6.2.6.1 6.2.7 6.2.8 6.2.9 6.3.0 6.3.1 6.3.10.2 6.3.11 6.3.12 6.3.2 6.3.3 6.3.4 6.3.5 6.3.6 6.3.6.1 6.4.0 6.4.0.1 6.4.1 6.4.2 6.4.3 6.5.0 6.5.1 6.6.0 6.6.1 6.6.2 6.7.0 6.7.1 6.7.2 6.8.0 trunk 1.0.0 1.0.2 1.0.3 1.0.5 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4 2.0.5 2.1.1 2.1.3 2.1.4 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.0.6 3.0.7 3.1.0 3.1.1 3.1.2 3.1.3 3.1.4 3.1.5 3.1.6 3.1.7 3.1.8 3.1.9 3.2.0 3.2.2 3.2.3 3.2.4 3.2.5 3.2.6 3.2.7 3.2.8 3.2.9 3.3.0 3.3.1 3.3.2 3.3.3 3.3.4 3.3.5 3.3.6 3.3.7 3.3.8 3.3.9 3.4.0 3.4.1 3.4.2 3.4.3 3.5.0 3.5.1 3.5.2 3.5.3 3.5.4 3.5.5 3.5.6 3.5.7 3.5.8 4.0.0 4.0.1 4.0.2 4.0.3 4.1.0 4.1.1 4.1.2 4.1.3 4.1.4 4.1.5 4.1.6 4.1.8 4.2.0 4.2.1 4.2.2 4.3.0 4.3.1 4.3.2 4.3.3 4.3.4 4.3.5 4.3.6 4.3.7 4.3.8 4.3.9 4.4.0 4.4.1 4.4.10 4.4.11 4.4.12 4.4.2 4.4.3 4.4.4 4.4.5 4.4.6 4.4.7 4.4.8 4.4.9 5.10 5.10.1 5.10.2 5.11 5.11.1 5.11.2 5.11.3 5.11.4 5.12 5.12.1 5.12.2 5.12.3 5.12.4 5.12.5 5.12.6 5.6.10 5.6.2 5.6.3 5.6.4 5.6.5 5.6.6 5.6.7 5.6.8 5.6.9 5.7.0 5.7.1 5.7.10 5.7.12 5.7.13 5.7.2 5.7.3 5.7.4 5.7.5 5.7.6 5.7.7 5.7.8 5.7.9 5.8.0 5.8.1 5.8.10 5.8.11 5.8.12 5.8.13 5.8.14 5.8.2 5.8.3 5.8.4
advanced-custom-fields / includes / post-types / class-acf-post-type.php
advanced-custom-fields / includes / post-types Last commit date
class-acf-field-group.php 5 months ago class-acf-post-type.php 5 months ago class-acf-taxonomy.php 5 months ago index.php 2 years ago
class-acf-post-type.php
975 lines
1 <?php
2 /**
3 * @package ACF
4 * @author WP Engine
5 *
6 * © 2026 Advanced Custom Fields (ACF®). All rights reserved.
7 * "ACF" is a trademark of WP Engine.
8 * Licensed under the GNU General Public License v2 or later.
9 * https://www.gnu.org/licenses/gpl-2.0.html
10 */
11
12 if ( ! defined( 'ABSPATH' ) ) {
13 exit; // Exit if accessed directly.
14 }
15
16 if ( ! class_exists( 'ACF_Post_Type' ) ) {
17 class ACF_Post_Type extends ACF_Internal_Post_Type {
18
19 /**
20 * The ACF internal post type name.
21 *
22 * @var string
23 */
24 public $post_type = 'acf-post-type';
25
26 /**
27 * The prefix for the key used in the main post array.
28 *
29 * @var string
30 */
31 public $post_key_prefix = 'post_type_';
32
33 /**
34 * The cache key for a singular post.
35 *
36 * @var string
37 */
38 public $cache_key = 'acf_get_post_type_post:key:';
39
40 /**
41 * The cache key for a collection of posts.
42 *
43 * @var string
44 */
45 public $cache_key_plural = 'acf_get_post_type_posts';
46
47 /**
48 * The hook name for a singular post.
49 *
50 * @var string
51 */
52 public $hook_name = 'post_type';
53
54 /**
55 * The hook name for a collection of posts.
56 *
57 * @var string
58 */
59 public $hook_name_plural = 'post_types';
60
61 /**
62 * The name of the store used for the post type.
63 *
64 * @var string
65 */
66 public $store = 'post-types';
67
68 /**
69 * Constructs the class.
70 */
71 public function __construct() {
72 $this->register_post_type();
73
74 // Include admin classes in admin.
75 if ( is_admin() ) {
76 acf_include( 'includes/admin/admin-internal-post-type-list.php' );
77 acf_include( 'includes/admin/admin-internal-post-type.php' );
78 acf_include( 'includes/admin/post-types/admin-post-type.php' );
79 acf_include( 'includes/admin/post-types/admin-post-types.php' );
80 }
81
82 parent::__construct();
83
84 add_action( 'acf/init', array( $this, 'register_post_types' ), 6 );
85 add_filter( 'enter_title_here', array( $this, 'enter_title_here' ), 10, 2 );
86 }
87
88 /**
89 * Registers the acf-post-type custom post type with WordPress.
90 *
91 * @since 6.1
92 */
93 public function register_post_type() {
94 $cap = acf_get_setting( 'capability' );
95
96 register_post_type(
97 'acf-post-type',
98 array(
99 'labels' => array(
100 'name' => __( 'Post Types', 'acf' ),
101 'singular_name' => __( 'Post Type', 'acf' ),
102 'add_new' => __( 'Add New', 'acf' ),
103 'add_new_item' => __( 'Add New Post Type', 'acf' ),
104 'edit_item' => __( 'Edit Post Type', 'acf' ),
105 'new_item' => __( 'New Post Type', 'acf' ),
106 'view_item' => __( 'View Post Type', 'acf' ),
107 'search_items' => __( 'Search Post Types', 'acf' ),
108 'not_found' => __( 'No Post Types found', 'acf' ),
109 'not_found_in_trash' => __( 'No Post Types found in Trash', 'acf' ),
110 ),
111 'public' => false,
112 'hierarchical' => true,
113 'show_ui' => true,
114 'show_in_menu' => false,
115 '_builtin' => false,
116 'capability_type' => 'post',
117 'capabilities' => array(
118 'edit_post' => $cap,
119 'delete_post' => $cap,
120 'edit_posts' => $cap,
121 'delete_posts' => $cap,
122 ),
123 'supports' => false,
124 'rewrite' => false,
125 'query_var' => false,
126 )
127 );
128 }
129
130 /**
131 * Register activated post types with WordPress
132 *
133 * @since 6.1
134 */
135 public function register_post_types() {
136 foreach ( $this->get_posts( array( 'active' => true ) ) as $post_type ) {
137 $post_type_key = $post_type['post_type'];
138 $post_type_args = $this->get_post_type_args( $post_type );
139
140 if ( ! post_type_exists( $post_type_key ) ) {
141 register_post_type( $post_type_key, $post_type_args );
142 } else {
143 // Flag on the store we bailed on registering this as it already exists.
144 $store = acf_get_store( $this->store );
145 $store_value = $store->get( $post_type['key'] );
146 $store_value['not_registered'] = true;
147 $store->set( $post_type['key'], $store_value );
148 }
149 }
150 }
151
152 /**
153 * Filters the "Add title" text for ACF post types.
154 *
155 * @since 6.2.1
156 *
157 * @param string $default The default text.
158 * @param WP_Post $post The WP_Post object.
159 * @return string
160 */
161 public function enter_title_here( $default, $post ) {
162 $post_types = $this->get_posts( array( 'active' => true ) );
163
164 foreach ( $post_types as $post_type ) {
165 if ( $post->post_type === $post_type['post_type'] && ! empty( $post_type['enter_title_here'] ) ) {
166 return (string) $post_type['enter_title_here'];
167 }
168 }
169
170 return $default;
171 }
172
173 /**
174 * Gets the default settings array for an ACF post type.
175 *
176 * @return array
177 */
178 public function get_settings_array() {
179 return array(
180 // ACF-specific settings.
181 'ID' => 0,
182 'key' => '',
183 'title' => '',
184 'menu_order' => 0,
185 'active' => true,
186 'post_type' => '', // First $post_type param passed to register_post_type().
187 'advanced_configuration' => false,
188 'import_source' => '',
189 'import_date' => '',
190 'allow_ai_access' => false,
191 'ai_description' => '',
192 // Settings passed to register_post_type().
193 'labels' => array(
194 'name' => '',
195 'singular_name' => '',
196 'menu_name' => '',
197 'all_items' => '',
198 'add_new' => '',
199 'add_new_item' => '',
200 'edit_item' => '',
201 'new_item' => '',
202 'view_item' => '',
203 'view_items' => '',
204 'search_items' => '',
205 'not_found' => '',
206 'not_found_in_trash' => '',
207 'parent_item_colon' => '',
208 'archives' => '',
209 'attributes' => '',
210 'featured_image' => '',
211 'set_featured_image' => '',
212 'remove_featured_image' => '',
213 'use_featured_image' => '',
214 'insert_into_item' => '',
215 'uploaded_to_this_item' => '',
216 'filter_items_list' => '',
217 'filter_by_date' => '',
218 'items_list_navigation' => '',
219 'items_list' => '',
220 'item_published' => '',
221 'item_published_privately' => '',
222 'item_reverted_to_draft' => '',
223 'item_scheduled' => '',
224 'item_updated' => '',
225 'item_link' => '',
226 'item_link_description' => '',
227 ),
228 'description' => '',
229 'public' => true, // WP defaults false, ACF defaults true.
230 'hierarchical' => false,
231 'exclude_from_search' => false,
232 'publicly_queryable' => true,
233 'show_ui' => true,
234 'show_in_menu' => true,
235 'admin_menu_parent' => '',
236 'show_in_admin_bar' => true,
237 'show_in_nav_menus' => true,
238 'show_in_rest' => true,
239 'rest_base' => '',
240 'rest_namespace' => 'wp/v2',
241 'rest_controller_class' => 'WP_REST_Posts_Controller',
242 'menu_position' => null,
243 'menu_icon' => '',
244 'rename_capabilities' => false,
245 'singular_capability_name' => 'post',
246 'plural_capability_name' => 'posts',
247 'supports' => array( 'title', 'editor', 'thumbnail', 'custom-fields' ),
248 'taxonomies' => array(),
249 'has_archive' => false,
250 'has_archive_slug' => '',
251 'rewrite' => array(
252 'permalink_rewrite' => 'post_type_key', // ACF-specific option.
253 'slug' => '',
254 'feeds' => false,
255 'pages' => true,
256 'with_front' => true,
257 ),
258 'query_var' => 'post_type_key',
259 'query_var_name' => '', // ACF-specific option.
260 'can_export' => true,
261 'delete_with_user' => false,
262 'register_meta_box_cb' => '',
263 'enter_title_here' => '',
264 );
265 }
266
267 /**
268 * Validates an ACF internal post type.
269 *
270 * @since 6.1
271 *
272 * @param array $post The main post array.
273 * @return array
274 */
275 public function validate_post( $post = array() ) {
276 // Bail early if already valid.
277 if ( is_array( $post ) && ! empty( $post['_valid'] ) ) {
278 return $post;
279 }
280
281 $defaults = $this->get_settings_array();
282 $post = wp_parse_args(
283 $post,
284 $defaults
285 );
286
287 // Convert types.
288 $post['ID'] = (int) $post['ID'];
289 $post['menu_order'] = (int) $post['menu_order'];
290
291 foreach ( $post as $setting => $value ) {
292 if ( isset( $defaults[ $setting ] ) ) {
293 $default_type = gettype( $defaults[ $setting ] );
294
295 // register_post_type() needs proper booleans.
296 if ( 'boolean' === $default_type && in_array( $value, array( '0', '1' ), true ) ) {
297 $post[ $setting ] = (bool) $value;
298 }
299
300 if ( 'boolean' === $default_type && in_array( $value, array( 'false', 'true' ), true ) ) {
301 $post[ $setting ] = ! ( 'false' === $value );
302 }
303 }
304 }
305
306 // Post is now valid.
307 $post['_valid'] = true;
308
309 /**
310 * Filters the ACF post array to validate settings.
311 *
312 * @date 12/02/2014
313 * @since 5.0.0
314 *
315 * @param array $post The post array.
316 */
317 return apply_filters( "acf/validate_{$this->hook_name}", $post );
318 }
319
320 /**
321 * Validates post type values before allowing save from the global $_POST object.
322 * Errors are added to the form using acf_add_internal_post_type_validation_error().
323 *
324 * @since 6.1
325 *
326 * @return boolean validity status
327 */
328 public function ajax_validate_values() {
329 if ( empty( $_POST['acf_post_type']['post_type'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Verified elsewhere.
330 return false;
331 }
332
333 $post_type_key = acf_sanitize_request_args( wp_unslash( $_POST['acf_post_type']['post_type'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Verified elsewhere.
334 $post_type_key = is_string( $post_type_key ) ? $post_type_key : '';
335 $valid = true;
336
337 if ( strlen( $post_type_key ) > 20 ) {
338 $valid = false;
339 acf_add_internal_post_type_validation_error( 'post_type', __( 'The post type key must be under 20 characters.', 'acf' ) );
340 }
341
342 if ( preg_match( '/^[a-z0-9_-]*$/', $post_type_key ) !== 1 ) {
343 $valid = false;
344 acf_add_internal_post_type_validation_error( 'post_type', __( 'The post type key must only contain lower case alphanumeric characters, underscores or dashes.', 'acf' ) );
345 }
346
347 if ( in_array( $post_type_key, acf_get_wp_reserved_terms(), true ) ) {
348 $valid = false;
349 /* translators: %s a link to WordPress.org's Reserved Terms page */
350 $message = sprintf( __( 'This field must not be a WordPress <a href="%s" target="_blank">reserved term</a>.', 'acf' ), 'https://codex.wordpress.org/Reserved_Terms' );
351 acf_add_internal_post_type_validation_error( 'post_type', $message );
352 } else {
353 // Check if this post key exists in the ACF store for registered post types, excluding those which failed registration.
354 $store = acf_get_store( $this->store );
355 $post_id = (int) acf_maybe_get_POST( 'post_id', 0 );
356
357 $matches = array_filter(
358 $store->get_data(),
359 function ( $item ) use ( $post_type_key ) {
360 return $item['post_type'] === $post_type_key && empty( $item['not_registered'] );
361 }
362 );
363 $duplicates = array_filter(
364 $matches,
365 function ( $item ) use ( $post_id ) {
366 return $item['ID'] !== $post_id;
367 }
368 );
369
370 if ( $duplicates ) {
371 $valid = false;
372 acf_add_internal_post_type_validation_error( 'post_type', __( 'This post type key is already in use by another post type in ACF and cannot be used.', 'acf' ) );
373 } else {
374 // If we're not already in use with another ACF post type, check if we're registered, but not by ACF.
375 if ( empty( $matches ) && post_type_exists( $post_type_key ) ) {
376 $valid = false;
377 acf_add_internal_post_type_validation_error( 'post_type', __( 'This post type key is already in use by another post type registered outside of ACF and cannot be used.', 'acf' ) );
378 }
379 }
380 }
381
382 $valid = apply_filters( "acf/{$this->hook_name}/ajax_validate_values", $valid, $_POST['acf_post_type'] ); // phpcs:ignore WordPress.Security -- Raw input send to hook for validation.
383
384 return $valid;
385 }
386
387 /**
388 * Parses ACF post type settings and returns an array of post type
389 * args that can be easily handled by `register_post_type()`.
390 *
391 * Omits settings that line up with the WordPress defaults to reduce the size
392 * of the array passed to `register_post_type()`, which might be exported.
393 *
394 * @since 6.1
395 *
396 * @param array $post The main ACF post type settings array.
397 * @param boolean $escape_labels Determines if the label values should be escaped.
398 * @return array
399 */
400 public function get_post_type_args( $post, $escape_labels = true ) {
401 $args = array();
402
403 // Make sure any provided labels are escaped strings and not empty.
404 $labels = array_filter( $post['labels'] );
405 $labels = array_map( 'strval', $labels );
406 if ( $escape_labels ) {
407 $labels = array_map( 'esc_html', $labels );
408 }
409 if ( ! empty( $labels ) ) {
410 $args['labels'] = $labels;
411 }
412
413 // Description is an optional string.
414 if ( ! empty( $post['description'] ) ) {
415 $args['description'] = (string) $post['description'];
416 }
417
418 // ACF requires the public setting to decide other settings.
419 $args['public'] = ! empty( $post['public'] );
420
421 // WordPress and ACF both default to false, so this can be omitted if still false.
422 if ( ! empty( $post['hierarchical'] ) ) {
423 $args['hierarchical'] = true;
424 }
425
426 // WordPress defaults to the opposite of $args['public'].
427 $exclude_from_search = (bool) $post['exclude_from_search'];
428 if ( $exclude_from_search === $args['public'] ) {
429 $args['exclude_from_search'] = $exclude_from_search;
430 }
431
432 // WordPress defaults to the same as $args['public'].
433 $publicly_queryable = (bool) $post['publicly_queryable'];
434 if ( $publicly_queryable !== $args['public'] ) {
435 $args['publicly_queryable'] = $publicly_queryable;
436 }
437
438 // WordPress defaults to the same as $args['public'].
439 $show_ui = (bool) $post['show_ui'];
440 if ( $show_ui !== $args['public'] ) {
441 $args['show_ui'] = $show_ui;
442 }
443
444 // WordPress defaults to the same as $args['show_ui'], can be string or boolean.
445 $show_in_menu = (bool) $post['show_in_menu'];
446 if ( $show_in_menu !== $show_ui ) {
447 $args['show_in_menu'] = $show_in_menu;
448 }
449
450 // WordPress also accepts a string for $args['show_in_menu'] to change the menu parent.
451 if ( $show_in_menu && ! empty( $post['admin_menu_parent'] ) ) {
452 $args['show_in_menu'] = (string) $post['admin_menu_parent'];
453 }
454
455 // WordPress defaults to the same as $args['public'].
456 $show_in_nav_menus = (bool) $post['show_in_nav_menus'];
457 if ( $show_in_nav_menus !== $args['public'] ) {
458 $args['show_in_nav_menus'] = $show_in_nav_menus;
459 }
460
461 // WordPress defaults to the same as $show_in_menu.
462 $show_in_admin_bar = (bool) $post['show_in_admin_bar'];
463 if ( $show_in_admin_bar !== $show_in_menu ) {
464 $args['show_in_admin_bar'] = $show_in_admin_bar;
465 }
466
467 // ACF defaults to true, but can be overridden.
468 $show_in_rest = (bool) $post['show_in_rest'];
469 $args['show_in_rest'] = $show_in_rest;
470
471 // WordPress defaults to $post_type.
472 $rest_base = (string) $post['rest_base'];
473 if ( ! empty( $rest_base ) && $rest_base !== $post['post_type'] ) {
474 $args['rest_base'] = $rest_base;
475 }
476
477 // WordPress defaults to "wp/v2".
478 $rest_namespace = (string) $post['rest_namespace'];
479 if ( ! empty( $rest_namespace ) && 'wp/v2' !== $rest_namespace ) {
480 $args['rest_namespace'] = $post['rest_namespace'];
481 }
482
483 // WordPress defaults to "WP_REST_Posts_Controller".
484 $rest_controller_class = (string) $post['rest_controller_class'];
485 if ( ! empty( $rest_controller_class ) && 'WP_REST_Posts_Controller' !== $rest_controller_class ) {
486 $args['rest_controller_class'] = $rest_controller_class;
487 }
488
489 // WordPress defaults to `null` (below the comments menu item).
490 $menu_position = (int) $post['menu_position'];
491 if ( $menu_position ) {
492 $args['menu_position'] = $menu_position;
493 }
494
495 // Set the default for the icon.
496 $args['menu_icon'] = 'dashicons-admin-post';
497
498 // Override that default if a value is provided.
499 if ( ! empty( $post['menu_icon'] ) ) {
500 if ( is_string( $post['menu_icon'] ) ) {
501 $args['menu_icon'] = $post['menu_icon'];
502 }
503 if ( is_array( $post['menu_icon'] ) ) {
504 if ( $post['menu_icon']['type'] === 'media_library' ) {
505 $args['menu_icon'] = wp_get_attachment_image_url( $post['menu_icon']['value'] );
506 } else {
507 $args['menu_icon'] = $post['menu_icon']['value'];
508 }
509 }
510 }
511
512 // WordPress defaults to "post" for `$args['capability_type']`, but can also take an array.
513 $rename_capabilities = (bool) $post['rename_capabilities'];
514 if ( $rename_capabilities ) {
515 $singular_capability_name = (string) $post['singular_capability_name'];
516 $plural_capability_name = (string) $post['plural_capability_name'];
517 $capability_type = 'post';
518
519 if ( ! empty( $singular_capability_name ) && ! empty( $plural_capability_name ) ) {
520 $capability_type = array( $singular_capability_name, $plural_capability_name );
521 } elseif ( ! empty( $singular_capability_name ) ) {
522 $capability_type = $singular_capability_name;
523 }
524
525 if ( $capability_type !== 'post' && $capability_type !== array( 'post', 'posts' ) ) {
526 $args['capability_type'] = $capability_type;
527 $args['map_meta_cap'] = true;
528 }
529 }
530
531 // TODO: We don't handle the `capabilities` arg at the moment, but may in the future.
532 // WordPress defaults to the "title" and "editor" supports, but none can be provided by passing false (WP 3.5+).
533 $supports = is_array( $post['supports'] ) ? $post['supports'] : array();
534 $supports = array_unique( array_filter( array_map( 'strval', $supports ) ) );
535
536 // Notes is a sub-feature of the editor support in WP 6.9+.
537 // When both editor and notes are enabled, convert to the proper format.
538 $has_notes = in_array( 'notes', $supports, true );
539 $has_editor = in_array( 'editor', $supports, true );
540
541 if ( $has_notes && $has_editor ) {
542 // Remove both 'notes' and 'editor' so we can add 'editor' back as an associative array.
543 $supports = array_values( array_diff( $supports, array( 'notes', 'editor' ) ) );
544
545 // Add 'editor' with notes sub-feature enabled (WP 6.9+ format).
546 $supports['editor'] = array( 'notes' => true );
547 }
548
549 if ( empty( $supports ) ) {
550 $args['supports'] = false;
551 } else {
552 $args['supports'] = $supports;
553 }
554
555 // Handle register meta box callbacks safely
556 if ( ! empty( $post['register_meta_box_cb'] ) ) {
557 $args['register_meta_box_cb'] = array( $this, 'build_safe_context_for_metabox_cb' );
558 }
559
560 // WordPress doesn't register any default taxonomies.
561 $taxonomies = $post['taxonomies'];
562 if ( ! is_array( $taxonomies ) ) {
563 $taxonomies = (array) $taxonomies;
564 }
565
566 $taxonomies = array_filter( $taxonomies );
567 if ( ! empty( $taxonomies ) ) {
568 $args['taxonomies'] = $taxonomies;
569 }
570
571 // WordPress and ACF default to false, true or a string can also be provided.
572 $has_archive = (bool) $post['has_archive'];
573 if ( $has_archive ) {
574 $has_archive_slug = (string) $post['has_archive_slug'];
575
576 if ( ! empty( $has_archive_slug ) ) {
577 $args['has_archive'] = $has_archive_slug;
578 } else {
579 $args['has_archive'] = true;
580 }
581 }
582
583 // The rewrite arg can be a boolean or array of further settings. WordPress and ACF default to true.
584 $rewrite = (array) $post['rewrite'];
585 $rewrite_enabled = true;
586 $rewrite_args = array();
587
588 // ACF-specific select, defaults to "post_type_key".
589 $rewrite['permalink_rewrite'] = isset( $rewrite['permalink_rewrite'] ) ? (string) $rewrite['permalink_rewrite'] : 'post_type_key';
590
591 if ( 'no_permalink' === $rewrite['permalink_rewrite'] ) {
592 $rewrite_enabled = false;
593 }
594
595 // Rewrite slug defaults to $post_type key if custom rewrites are not enabled.
596 if ( ! empty( $rewrite['slug'] ) && $rewrite['slug'] !== $post['post_type'] && 'custom_permalink' === $rewrite['permalink_rewrite'] ) {
597 $rewrite_args['slug'] = (string) $rewrite['slug'];
598 }
599
600 // WordPress defaults to true.
601 if ( isset( $rewrite['with_front'] ) && ! $rewrite['with_front'] && $rewrite_enabled ) {
602 $rewrite_args['with_front'] = false;
603 }
604
605 // WordPress defaults to value of `$args['has_archive']`.
606 if ( isset( $rewrite['feeds'] ) && (bool) $rewrite['feeds'] !== $has_archive && $rewrite_enabled ) {
607 $rewrite_args['feeds'] = (bool) $rewrite['feeds'];
608 }
609
610 // WordPress defaults to true.
611 if ( isset( $rewrite['pages'] ) && ! $rewrite['pages'] && $rewrite_enabled ) {
612 $rewrite_args['pages'] = false;
613 }
614
615 // Assemble rewrite args.
616 if ( ! empty( $rewrite_args ) ) {
617 $args['rewrite'] = $rewrite_args;
618 } elseif ( ! $rewrite_enabled ) {
619 $args['rewrite'] = false;
620 }
621
622 // WordPress and ACF default to $post_type key, a boolean can also be used.
623 $query_var = (string) $post['query_var'];
624 if ( 'custom_query_var' === $query_var ) {
625 $query_var_name = (string) $post['query_var_name'];
626
627 if ( ! empty( $query_var_name ) && $query_var_name !== $post['post_type'] ) {
628 $args['query_var'] = $query_var_name;
629 }
630 } elseif ( 'none' === $query_var ) {
631 $args['query_var'] = false;
632 }
633
634 // WordPress and ACF default to true.
635 $can_export = (bool) $post['can_export'];
636 if ( ! $can_export ) {
637 $args['can_export'] = false;
638 }
639
640 // ACF defaults to false, while WordPress defaults to omitting (deletes only if author support is added).
641 $args['delete_with_user'] = (bool) $post['delete_with_user'];
642
643 return apply_filters( 'acf/post_type/registration_args', $args, $post );
644 }
645
646 /**
647 * Ensure the metabox being called does not perform any unsafe operations.
648 *
649 * @since 6.3.8
650 *
651 * @param WP_Post $post The post being rendered.
652 * @return mixed The callback result.
653 */
654 public function build_safe_context_for_metabox_cb( $post ) {
655 $post_types = $this->get_posts();
656 $this_post = array_filter(
657 $post_types,
658 function ( $post_type ) use ( $post ) {
659 return $post_type['post_type'] === $post->post_type;
660 }
661 );
662 if ( empty( $this_post ) || ! is_array( $this_post ) ) {
663 // Unable to find the ACF post type. Don't do anything.
664 return;
665 }
666 $acf_post_type = array_shift( $this_post );
667 $original_cb = isset( $acf_post_type['register_meta_box_cb'] ) ? $acf_post_type['register_meta_box_cb'] : false;
668
669 // Prevent access to any wp_ prefixed functions in a callback.
670 if ( apply_filters( 'acf/post_type/prevent_access_to_wp_functions_in_meta_box_cb', true ) && substr( strtolower( $original_cb ), 0, 3 ) === 'wp_' ) {
671 // Don't execute register meta box callbacks if an internal wp function by default.
672 return;
673 }
674
675 $unset = array( '_POST', '_GET', '_REQUEST', '_COOKIE', '_SESSION', '_FILES', '_ENV', '_SERVER' );
676 $originals = array();
677
678 foreach ( $unset as $var ) {
679 if ( isset( $GLOBALS[ $var ] ) ) {
680 $originals[ $var ] = $GLOBALS[ $var ];
681 $GLOBALS[ $var ] = array(); //phpcs:ignore -- used for building a safe context
682 }
683 }
684
685 $return = false;
686 if ( is_callable( $original_cb ) ) {
687 $return = call_user_func( $original_cb, $post );
688 }
689
690 foreach ( $unset as $var ) {
691 if ( isset( $originals[ $var ] ) ) {
692 $GLOBALS[ $var ] = $originals[ $var ]; //phpcs:ignore -- used for restoring the original context
693 }
694 }
695
696 return $return;
697 }
698
699 /**
700 * Returns a string that can be used to create a post type in PHP.
701 *
702 * @since 6.1
703 *
704 * @param array $post The main post type array.
705 * @return string
706 */
707 public function export_post_as_php( $post = array() ) {
708 $return = '';
709 if ( empty( $post ) ) {
710 return $return;
711 }
712
713 $post_type_key = $post['post_type'];
714
715 // Validate and prepare the post for export.
716 $post = $this->validate_post( $post );
717 $args = $this->get_post_type_args( $post, false );
718
719 // Restore original metabox callback.
720 if ( ! empty( $args['register_meta_box_cb'] ) && ! empty( $post['register_meta_box_cb'] ) ) {
721 $args['register_meta_box_cb'] = (string) $post['register_meta_box_cb'];
722 }
723
724 $code = var_export( $args, true ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions -- Used for PHP export.
725
726 if ( ! $code ) {
727 return $return;
728 }
729
730 $code = $this->format_code_for_export( $code );
731
732 $return .= "register_post_type( '{$post_type_key}', {$code} );\r\n";
733
734 return esc_textarea( $return );
735 }
736
737 /**
738 * Flush rewrite rules whenever anything changes about a post type.
739 *
740 * @since 6.1
741 *
742 * @param array $post The main post type array.
743 */
744 public function flush_post_cache( $post ) {
745 // Bail early if we won't be able to register/unregister the post type.
746 if ( empty( $post['post_type'] ) ) {
747 return;
748 }
749
750 // Temporarily unregister the post type so that we can potentially re-register with the latest args below.
751 if ( empty( $post['active'] ) || post_type_exists( $post['post_type'] ) ) {
752 unregister_post_type( $post['post_type'] );
753 }
754
755 // When this is being called, the post type may not have been registered yet, so we do it now.
756 if ( ! empty( $post['active'] ) ) {
757 register_post_type( $post['post_type'], $this->get_post_type_args( $post ) );
758 }
759
760 // Flush our internal cache and the WordPress rewrite rules.
761 parent::flush_post_cache( $post );
762 flush_rewrite_rules();
763 }
764
765 /**
766 * Translates an ACF post.
767 *
768 * @since 6.1
769 *
770 * @param array $post The field group array.
771 * @return array
772 */
773 public function translate_post( $post = array() ) {
774 // Get settings.
775 $l10n = acf_get_setting( 'l10n' );
776 $l10n_textdomain = acf_get_setting( 'l10n_textdomain' );
777
778 // Translate field settings if textdomain is set.
779 if ( $l10n && $l10n_textdomain ) {
780 $post['title'] = acf_translate( $post['title'] );
781 $post['description'] = acf_translate( $post['description'] );
782 foreach ( $post['labels'] as $key => $label ) {
783 $post['labels'][ $key ] = acf_translate( $label );
784 }
785
786 /**
787 * Filters the post array to translate strings.
788 *
789 * @date 12/02/2014
790 * @since 5.0.0
791 *
792 * @param array $post The post array.
793 */
794 $post = apply_filters( "acf/translate_{$this->hook_name}", $post );
795 }
796
797 return $post;
798 }
799
800 /**
801 * Prepares an ACF post type for import.
802 *
803 * @since 6.3.10
804 *
805 * @param array $post The ACF post array.
806 * @return array
807 */
808 public function prepare_post_for_import( $post ) {
809 if ( ! acf_get_setting( 'enable_meta_box_cb_edit' ) && ! empty( $post['register_meta_box_cb'] ) ) {
810 $post['register_meta_box_cb'] = '';
811
812 if ( ! empty( $post['ID'] ) ) {
813 $existing_post = $this->get_post( $post['ID'] );
814
815 if ( is_array( $existing_post ) ) {
816 $post['register_meta_box_cb'] = ! empty( $existing_post['register_meta_box_cb'] ) ? (string) $existing_post['register_meta_box_cb'] : '';
817 }
818 }
819 }
820
821 return parent::prepare_post_for_import( $post );
822 }
823
824 /**
825 * Imports a post type from CPTUI.
826 *
827 * @since 6.1
828 *
829 * @param array $args Arguments from CPTUI.
830 * @return array
831 */
832 public function import_cptui_post_type( $args ) {
833 $acf_args = $this->get_settings_array();
834
835 // Convert string boolean values to proper booleans.
836 foreach ( $args as $key => $value ) {
837 if ( in_array( $value, array( 'true', 'false' ), true ) ) {
838 $args[ $key ] = filter_var( $value, FILTER_VALIDATE_BOOLEAN );
839 }
840 }
841
842 if ( isset( $args['name'] ) ) {
843 $acf_args['post_type'] = (string) $args['name'];
844 unset( $args['name'] );
845 }
846
847 if ( isset( $args['labels'] ) ) {
848 $acf_args['labels'] = array_merge( $acf_args['labels'], $args['labels'] );
849 unset( $args['labels'] );
850 }
851
852 // ACF uses "name" as title, and stores in labels array.
853 if ( isset( $args['label'] ) ) {
854 $acf_args['title'] = (string) $args['label'];
855 $acf_args['labels']['name'] = (string) $args['label'];
856 unset( $args['label'] );
857 }
858
859 if ( isset( $args['singular_label'] ) ) {
860 $acf_args['labels']['singular_name'] = (string) $args['singular_label'];
861 unset( $args['singular_label'] );
862 }
863
864 if ( isset( $args['show_in_menu_string'] ) ) {
865 $acf_args['admin_menu_parent'] = (string) $args['show_in_menu_string'];
866 unset( $args['show_in_menu_string'] );
867 }
868
869 if ( isset( $args['rewrite'] ) ) {
870 $rewrite = (bool) $args['rewrite'];
871
872 if ( ! $rewrite ) {
873 $acf_args['rewrite']['permalink_rewrite'] = 'no_permalink';
874 } elseif ( ! empty( $args['rewrite_slug'] ) ) {
875 $acf_args['rewrite']['permalink_rewrite'] = 'custom_permalink';
876 } else {
877 $acf_args['rewrite']['permalink_rewrite'] = 'post_type_key';
878 }
879
880 unset( $args['rewrite'] );
881 }
882
883 if ( isset( $args['rewrite_slug'] ) ) {
884 $acf_args['rewrite']['slug'] = (string) $args['rewrite_slug'];
885 unset( $args['rewrite_slug'] );
886 }
887
888 if ( isset( $args['rewrite_withfront'] ) ) {
889 $acf_args['rewrite']['with_front'] = (bool) $args['rewrite_withfront'];
890 unset( $args['rewrite_withfront'] );
891 }
892
893 // TODO: Investigate CPTUI usage of with_feeds, pages settings.
894 // ACF handles capability type differently.
895 if ( isset( $args['capability_type'] ) ) {
896 if ( 'post' !== trim( $args['capability_type'] ) ) {
897 $acf_args['rename_capabilities'] = true;
898
899 $capabilities = explode( ',', $args['capability_type'] );
900 $capabilities = array_map( 'trim', $capabilities );
901
902 $acf_args['singular_capability_name'] = $capabilities[0];
903
904 if ( count( $capabilities ) > 1 ) {
905 $acf_args['plural_capability_name'] = $capabilities[1];
906 }
907 }
908
909 unset( $args['capability_type'] );
910 }
911
912 // ACF names the has_archive slug differently.
913 if ( isset( $args['has_archive_string'] ) ) {
914 $acf_args['has_archive_slug'] = (string) $args['has_archive_string'];
915 unset( $args['has_archive_string'] );
916 }
917
918 // ACF handles the query var and query var slug/name differently.
919 if ( isset( $args['query_var'] ) ) {
920 if ( ! $args['query_var'] ) {
921 $acf_args['query_var'] = 'none';
922 } elseif ( ! empty( $args['query_var_slug'] ) ) {
923 $acf_args['query_var'] = 'custom_query_var';
924 } else {
925 $acf_args['query_var'] = 'post_type_key';
926 }
927
928 unset( $args['query_var'] );
929 }
930
931 if ( isset( $args['query_var_slug'] ) ) {
932 $acf_args['query_var_name'] = (string) $args['query_var_slug'];
933 unset( $args['query_var_slug'] );
934 }
935
936 $acf_args = wp_parse_args( $args, $acf_args );
937
938 // ACF doesn't yet handle custom supports, so we're tacking onto the regular supports.
939 if ( isset( $args['custom_supports'] ) ) {
940 $custom_supports = explode( ',', (string) $args['custom_supports'] );
941 $custom_supports = array_filter( array_map( 'trim', $custom_supports ) );
942
943 if ( ! empty( $custom_supports ) ) {
944 $acf_args['supports'] = array_merge( $acf_args['supports'], $custom_supports );
945 }
946
947 unset( $acf_args['custom_supports'] );
948 }
949
950 $acf_args['key'] = uniqid( 'post_type_' );
951 $acf_args['advanced_configuration'] = true;
952 $acf_args['import_source'] = 'cptui';
953 $acf_args['import_date'] = time();
954
955 $existing_post_types = acf_get_acf_post_types();
956
957 foreach ( $existing_post_types as $existing_post_type ) {
958 // Post type already exists, so we need to update rather than import.
959 if ( $acf_args['post_type'] === $existing_post_type['post_type'] ) {
960 $acf_args = $this->prepare_post_for_import( $acf_args );
961 $acf_args['ID'] = $existing_post_type['ID'];
962 $acf_args['key'] = $existing_post_type['key'];
963 return $this->update_post( $acf_args );
964 }
965 }
966
967 // Import the post normally.
968 return $this->import_post( $acf_args );
969 }
970 }
971
972 }
973
974 acf_new_instance( 'ACF_Post_Type' );
975