<?php
/**
 * @package ACF
 * @author  WP Engine
 *
 * © 2026 Advanced Custom Fields (ACF®). All rights reserved.
 * "ACF" is a trademark of WP Engine.
 * Licensed under the GNU General Public License v2 or later.
 * https://www.gnu.org/licenses/gpl-2.0.html
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

if ( ! class_exists( 'ACF_Post_Type' ) ) {
	class ACF_Post_Type extends ACF_Internal_Post_Type {

		/**
		 * The ACF internal post type name.
		 *
		 * @var string
		 */
		public $post_type = 'acf-post-type';

		/**
		 * The prefix for the key used in the main post array.
		 *
		 * @var string
		 */
		public $post_key_prefix = 'post_type_';

		/**
		 * The cache key for a singular post.
		 *
		 * @var string
		 */
		public $cache_key = 'acf_get_post_type_post:key:';

		/**
		 * The cache key for a collection of posts.
		 *
		 * @var string
		 */
		public $cache_key_plural = 'acf_get_post_type_posts';

		/**
		 * The hook name for a singular post.
		 *
		 * @var string
		 */
		public $hook_name = 'post_type';

		/**
		 * The hook name for a collection of posts.
		 *
		 * @var string
		 */
		public $hook_name_plural = 'post_types';

		/**
		 * The name of the store used for the post type.
		 *
		 * @var string
		 */
		public $store = 'post-types';

		/**
		 * Constructs the class.
		 */
		public function __construct() {
			$this->register_post_type();

			// Include admin classes in admin.
			if ( is_admin() ) {
				acf_include( 'includes/admin/admin-internal-post-type-list.php' );
				acf_include( 'includes/admin/admin-internal-post-type.php' );
				acf_include( 'includes/admin/post-types/admin-post-type.php' );
				acf_include( 'includes/admin/post-types/admin-post-types.php' );
			}

			parent::__construct();

			add_action( 'acf/init', array( $this, 'register_post_types' ), 6 );
			add_filter( 'enter_title_here', array( $this, 'enter_title_here' ), 10, 2 );
		}

		/**
		 * Registers the acf-post-type custom post type with WordPress.
		 *
		 * @since 6.1
		 */
		public function register_post_type() {
			$cap = acf_get_setting( 'capability' );

			register_post_type(
				'acf-post-type',
				array(
					'labels'          => array(
						'name'               => __( 'Post Types', 'acf' ),
						'singular_name'      => __( 'Post Type', 'acf' ),
						'add_new'            => __( 'Add New', 'acf' ),
						'add_new_item'       => __( 'Add New Post Type', 'acf' ),
						'edit_item'          => __( 'Edit Post Type', 'acf' ),
						'new_item'           => __( 'New Post Type', 'acf' ),
						'view_item'          => __( 'View Post Type', 'acf' ),
						'search_items'       => __( 'Search Post Types', 'acf' ),
						'not_found'          => __( 'No Post Types found', 'acf' ),
						'not_found_in_trash' => __( 'No Post Types found in Trash', 'acf' ),
					),
					'public'          => false,
					'hierarchical'    => true,
					'show_ui'         => true,
					'show_in_menu'    => false,
					'_builtin'        => false,
					'capability_type' => 'post',
					'capabilities'    => array(
						'edit_post'    => $cap,
						'delete_post'  => $cap,
						'edit_posts'   => $cap,
						'delete_posts' => $cap,
					),
					'supports'        => false,
					'rewrite'         => false,
					'query_var'       => false,
				)
			);
		}

		/**
		 * Register activated post types with WordPress
		 *
		 * @since 6.1
		 */
		public function register_post_types() {
			foreach ( $this->get_posts( array( 'active' => true ) ) as $post_type ) {
				$post_type_key  = $post_type['post_type'];
				$post_type_args = $this->get_post_type_args( $post_type );

				if ( ! post_type_exists( $post_type_key ) ) {
					register_post_type( $post_type_key, $post_type_args );
				} else {
					// Flag on the store we bailed on registering this as it already exists.
					$store                         = acf_get_store( $this->store );
					$store_value                   = $store->get( $post_type['key'] );
					$store_value['not_registered'] = true;
					$store->set( $post_type['key'], $store_value );
				}
			}
		}

		/**
		 * Filters the "Add title" text for ACF post types.
		 *
		 * @since 6.2.1
		 *
		 * @param string  $default The default text.
		 * @param WP_Post $post    The WP_Post object.
		 * @return string
		 */
		public function enter_title_here( $default, $post ) {
			$post_types = $this->get_posts( array( 'active' => true ) );

			foreach ( $post_types as $post_type ) {
				if ( $post->post_type === $post_type['post_type'] && ! empty( $post_type['enter_title_here'] ) ) {
					return (string) $post_type['enter_title_here'];
				}
			}

			return $default;
		}

		/**
		 * Gets the default settings array for an ACF post type.
		 *
		 * @return array
		 */
		public function get_settings_array() {
			return array(
				// ACF-specific settings.
				'ID'                       => 0,
				'key'                      => '',
				'title'                    => '',
				'menu_order'               => 0,
				'active'                   => true,
				'post_type'                => '', // First $post_type param passed to register_post_type().
				'advanced_configuration'   => false,
				'import_source'            => '',
				'import_date'              => '',
				'allow_ai_access'          => false,
				'ai_description'           => '',
				// Settings passed to register_post_type().
				'labels'                   => array(
					'name'                     => '',
					'singular_name'            => '',
					'menu_name'                => '',
					'all_items'                => '',
					'add_new'                  => '',
					'add_new_item'             => '',
					'edit_item'                => '',
					'new_item'                 => '',
					'view_item'                => '',
					'view_items'               => '',
					'search_items'             => '',
					'not_found'                => '',
					'not_found_in_trash'       => '',
					'parent_item_colon'        => '',
					'archives'                 => '',
					'attributes'               => '',
					'featured_image'           => '',
					'set_featured_image'       => '',
					'remove_featured_image'    => '',
					'use_featured_image'       => '',
					'insert_into_item'         => '',
					'uploaded_to_this_item'    => '',
					'filter_items_list'        => '',
					'filter_by_date'           => '',
					'items_list_navigation'    => '',
					'items_list'               => '',
					'item_published'           => '',
					'item_published_privately' => '',
					'item_reverted_to_draft'   => '',
					'item_scheduled'           => '',
					'item_updated'             => '',
					'item_link'                => '',
					'item_link_description'    => '',
				),
				'description'              => '',
				'public'                   => true, // WP defaults false, ACF defaults true.
				'hierarchical'             => false,
				'exclude_from_search'      => false,
				'publicly_queryable'       => true,
				'show_ui'                  => true,
				'show_in_menu'             => true,
				'admin_menu_parent'        => '',
				'show_in_admin_bar'        => true,
				'show_in_nav_menus'        => true,
				'show_in_rest'             => true,
				'rest_base'                => '',
				'rest_namespace'           => 'wp/v2',
				'rest_controller_class'    => 'WP_REST_Posts_Controller',
				'menu_position'            => null,
				'menu_icon'                => '',
				'rename_capabilities'      => false,
				'singular_capability_name' => 'post',
				'plural_capability_name'   => 'posts',
				'supports'                 => array( 'title', 'editor', 'thumbnail', 'custom-fields' ),
				'taxonomies'               => array(),
				'has_archive'              => false,
				'has_archive_slug'         => '',
				'rewrite'                  => array(
					'permalink_rewrite' => 'post_type_key', // ACF-specific option.
					'slug'              => '',
					'feeds'             => false,
					'pages'             => true,
					'with_front'        => true,
				),
				'query_var'                => 'post_type_key',
				'query_var_name'           => '', // ACF-specific option.
				'can_export'               => true,
				'delete_with_user'         => false,
				'register_meta_box_cb'     => '',
				'enter_title_here'         => '',
			);
		}

		/**
		 * Validates an ACF internal post type.
		 *
		 * @since 6.1
		 *
		 * @param array $post The main post array.
		 * @return array
		 */
		public function validate_post( $post = array() ) {
			// Bail early if already valid.
			if ( is_array( $post ) && ! empty( $post['_valid'] ) ) {
				return $post;
			}

			$defaults = $this->get_settings_array();
			$post     = wp_parse_args(
				$post,
				$defaults
			);

			// Convert types.
			$post['ID']         = (int) $post['ID'];
			$post['menu_order'] = (int) $post['menu_order'];

			foreach ( $post as $setting => $value ) {
				if ( isset( $defaults[ $setting ] ) ) {
					$default_type = gettype( $defaults[ $setting ] );

					// register_post_type() needs proper booleans.
					if ( 'boolean' === $default_type && in_array( $value, array( '0', '1' ), true ) ) {
						$post[ $setting ] = (bool) $value;
					}

					if ( 'boolean' === $default_type && in_array( $value, array( 'false', 'true' ), true ) ) {
						$post[ $setting ] = ! ( 'false' === $value );
					}
				}
			}

			// Post is now valid.
			$post['_valid'] = true;

			/**
			 * Filters the ACF post array to validate settings.
			 *
			 * @date    12/02/2014
			 * @since   5.0.0
			 *
			 * @param   array $post The post array.
			 */
			return apply_filters( "acf/validate_{$this->hook_name}", $post );
		}

		/**
		 * Validates post type values before allowing save from the global $_POST object.
		 * Errors are added to the form using acf_add_internal_post_type_validation_error().
		 *
		 * @since 6.1
		 *
		 * @return boolean validity status
		 */
		public function ajax_validate_values() {
			if ( empty( $_POST['acf_post_type']['post_type'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Verified elsewhere.
				return false;
			}

			$post_type_key = acf_sanitize_request_args( wp_unslash( $_POST['acf_post_type']['post_type'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Verified elsewhere.
			$post_type_key = is_string( $post_type_key ) ? $post_type_key : '';
			$valid         = true;

			if ( strlen( $post_type_key ) > 20 ) {
				$valid = false;
				acf_add_internal_post_type_validation_error( 'post_type', __( 'The post type key must be under 20 characters.', 'acf' ) );
			}

			if ( preg_match( '/^[a-z0-9_-]*$/', $post_type_key ) !== 1 ) {
				$valid = false;
				acf_add_internal_post_type_validation_error( 'post_type', __( 'The post type key must only contain lower case alphanumeric characters, underscores or dashes.', 'acf' ) );
			}

			if ( in_array( $post_type_key, acf_get_wp_reserved_terms(), true ) ) {
				$valid = false;
				/* translators: %s a link to WordPress.org's Reserved Terms page */
				$message = sprintf( __( 'This field must not be a WordPress <a href="%s" target="_blank">reserved term</a>.', 'acf' ), 'https://codex.wordpress.org/Reserved_Terms' );
				acf_add_internal_post_type_validation_error( 'post_type', $message );
			} else {
				// Check if this post key exists in the ACF store for registered post types, excluding those which failed registration.
				$store   = acf_get_store( $this->store );
				$post_id = (int) acf_maybe_get_POST( 'post_id', 0 );

				$matches    = array_filter(
					$store->get_data(),
					function ( $item ) use ( $post_type_key ) {
						return $item['post_type'] === $post_type_key && empty( $item['not_registered'] );
					}
				);
				$duplicates = array_filter(
					$matches,
					function ( $item ) use ( $post_id ) {
						return $item['ID'] !== $post_id;
					}
				);

				if ( $duplicates ) {
					$valid = false;
					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' ) );
				} else {
					// If we're not already in use with another ACF post type, check if we're registered, but not by ACF.
					if ( empty( $matches ) && post_type_exists( $post_type_key ) ) {
						$valid = false;
						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' ) );
					}
				}
			}

			$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.

			return $valid;
		}

		/**
		 * Parses ACF post type settings and returns an array of post type
		 * args that can be easily handled by `register_post_type()`.
		 *
		 * Omits settings that line up with the WordPress defaults to reduce the size
		 * of the array passed to `register_post_type()`, which might be exported.
		 *
		 * @since 6.1
		 *
		 * @param  array   $post          The main ACF post type settings array.
		 * @param  boolean $escape_labels Determines if the label values should be escaped.
		 * @return array
		 */
		public function get_post_type_args( $post, $escape_labels = true ) {
			$args = array();

			// Make sure any provided labels are escaped strings and not empty.
			$labels = array_filter( $post['labels'] );
			$labels = array_map( 'strval', $labels );
			if ( $escape_labels ) {
				$labels = array_map( 'esc_html', $labels );
			}
			if ( ! empty( $labels ) ) {
				$args['labels'] = $labels;
			}

			// Description is an optional string.
			if ( ! empty( $post['description'] ) ) {
				$args['description'] = (string) $post['description'];
			}

			// ACF requires the public setting to decide other settings.
			$args['public'] = ! empty( $post['public'] );

			// WordPress and ACF both default to false, so this can be omitted if still false.
			if ( ! empty( $post['hierarchical'] ) ) {
				$args['hierarchical'] = true;
			}

			// WordPress defaults to the opposite of $args['public'].
			$exclude_from_search = (bool) $post['exclude_from_search'];
			if ( $exclude_from_search === $args['public'] ) {
				$args['exclude_from_search'] = $exclude_from_search;
			}

			// WordPress defaults to the same as $args['public'].
			$publicly_queryable = (bool) $post['publicly_queryable'];
			if ( $publicly_queryable !== $args['public'] ) {
				$args['publicly_queryable'] = $publicly_queryable;
			}

			// WordPress defaults to the same as $args['public'].
			$show_ui = (bool) $post['show_ui'];
			if ( $show_ui !== $args['public'] ) {
				$args['show_ui'] = $show_ui;
			}

			// WordPress defaults to the same as $args['show_ui'], can be string or boolean.
			$show_in_menu = (bool) $post['show_in_menu'];
			if ( $show_in_menu !== $show_ui ) {
				$args['show_in_menu'] = $show_in_menu;
			}

			// WordPress also accepts a string for $args['show_in_menu'] to change the menu parent.
			if ( $show_in_menu && ! empty( $post['admin_menu_parent'] ) ) {
				$args['show_in_menu'] = (string) $post['admin_menu_parent'];
			}

			// WordPress defaults to the same as $args['public'].
			$show_in_nav_menus = (bool) $post['show_in_nav_menus'];
			if ( $show_in_nav_menus !== $args['public'] ) {
				$args['show_in_nav_menus'] = $show_in_nav_menus;
			}

			// WordPress defaults to the same as $show_in_menu.
			$show_in_admin_bar = (bool) $post['show_in_admin_bar'];
			if ( $show_in_admin_bar !== $show_in_menu ) {
				$args['show_in_admin_bar'] = $show_in_admin_bar;
			}

			// ACF defaults to true, but can be overridden.
			$show_in_rest         = (bool) $post['show_in_rest'];
			$args['show_in_rest'] = $show_in_rest;

			// WordPress defaults to $post_type.
			$rest_base = (string) $post['rest_base'];
			if ( ! empty( $rest_base ) && $rest_base !== $post['post_type'] ) {
				$args['rest_base'] = $rest_base;
			}

			// WordPress defaults to "wp/v2".
			$rest_namespace = (string) $post['rest_namespace'];
			if ( ! empty( $rest_namespace ) && 'wp/v2' !== $rest_namespace ) {
				$args['rest_namespace'] = $post['rest_namespace'];
			}

			// WordPress defaults to "WP_REST_Posts_Controller".
			$rest_controller_class = (string) $post['rest_controller_class'];
			if ( ! empty( $rest_controller_class ) && 'WP_REST_Posts_Controller' !== $rest_controller_class ) {
				$args['rest_controller_class'] = $rest_controller_class;
			}

			// WordPress defaults to `null` (below the comments menu item).
			$menu_position = (int) $post['menu_position'];
			if ( $menu_position ) {
				$args['menu_position'] = $menu_position;
			}

			// Set the default for the icon.
			$args['menu_icon'] = 'dashicons-admin-post';

			// Override that default if a value is provided.
			if ( ! empty( $post['menu_icon'] ) ) {
				if ( is_string( $post['menu_icon'] ) ) {
					$args['menu_icon'] = $post['menu_icon'];
				}
				if ( is_array( $post['menu_icon'] ) ) {
					if ( $post['menu_icon']['type'] === 'media_library' ) {
						$args['menu_icon'] = wp_get_attachment_image_url( $post['menu_icon']['value'] );
					} else {
						$args['menu_icon'] = $post['menu_icon']['value'];
					}
				}
			}

			// WordPress defaults to "post" for `$args['capability_type']`, but can also take an array.
			$rename_capabilities = (bool) $post['rename_capabilities'];
			if ( $rename_capabilities ) {
				$singular_capability_name = (string) $post['singular_capability_name'];
				$plural_capability_name   = (string) $post['plural_capability_name'];
				$capability_type          = 'post';

				if ( ! empty( $singular_capability_name ) && ! empty( $plural_capability_name ) ) {
					$capability_type = array( $singular_capability_name, $plural_capability_name );
				} elseif ( ! empty( $singular_capability_name ) ) {
					$capability_type = $singular_capability_name;
				}

				if ( $capability_type !== 'post' && $capability_type !== array( 'post', 'posts' ) ) {
					$args['capability_type'] = $capability_type;
					$args['map_meta_cap']    = true;
				}
			}

			// TODO: We don't handle the `capabilities` arg at the moment, but may in the future.
			// WordPress defaults to the "title" and "editor" supports, but none can be provided by passing false (WP 3.5+).
			$supports = is_array( $post['supports'] ) ? $post['supports'] : array();
			$supports = array_unique( array_filter( array_map( 'strval', $supports ) ) );

			// Notes is a sub-feature of the editor support in WP 6.9+.
			// When both editor and notes are enabled, convert to the proper format.
			$has_notes  = in_array( 'notes', $supports, true );
			$has_editor = in_array( 'editor', $supports, true );

			if ( $has_notes && $has_editor ) {
				// Remove both 'notes' and 'editor' so we can add 'editor' back as an associative array.
				$supports = array_values( array_diff( $supports, array( 'notes', 'editor' ) ) );

				// Add 'editor' with notes sub-feature enabled (WP 6.9+ format).
				$supports['editor'] = array( 'notes' => true );
			}

			if ( empty( $supports ) ) {
				$args['supports'] = false;
			} else {
				$args['supports'] = $supports;
			}

			// Handle register meta box callbacks safely
			if ( ! empty( $post['register_meta_box_cb'] ) ) {
				$args['register_meta_box_cb'] = array( $this, 'build_safe_context_for_metabox_cb' );
			}

			// WordPress doesn't register any default taxonomies.
			$taxonomies = $post['taxonomies'];
			if ( ! is_array( $taxonomies ) ) {
				$taxonomies = (array) $taxonomies;
			}

			$taxonomies = array_filter( $taxonomies );
			if ( ! empty( $taxonomies ) ) {
				$args['taxonomies'] = $taxonomies;
			}

			// WordPress and ACF default to false, true or a string can also be provided.
			$has_archive = (bool) $post['has_archive'];
			if ( $has_archive ) {
				$has_archive_slug = (string) $post['has_archive_slug'];

				if ( ! empty( $has_archive_slug ) ) {
					$args['has_archive'] = $has_archive_slug;
				} else {
					$args['has_archive'] = true;
				}
			}

			// The rewrite arg can be a boolean or array of further settings. WordPress and ACF default to true.
			$rewrite         = (array) $post['rewrite'];
			$rewrite_enabled = true;
			$rewrite_args    = array();

			// ACF-specific select, defaults to "post_type_key".
			$rewrite['permalink_rewrite'] = isset( $rewrite['permalink_rewrite'] ) ? (string) $rewrite['permalink_rewrite'] : 'post_type_key';

			if ( 'no_permalink' === $rewrite['permalink_rewrite'] ) {
				$rewrite_enabled = false;
			}

			// Rewrite slug defaults to $post_type key if custom rewrites are not enabled.
			if ( ! empty( $rewrite['slug'] ) && $rewrite['slug'] !== $post['post_type'] && 'custom_permalink' === $rewrite['permalink_rewrite'] ) {
				$rewrite_args['slug'] = (string) $rewrite['slug'];
			}

			// WordPress defaults to true.
			if ( isset( $rewrite['with_front'] ) && ! $rewrite['with_front'] && $rewrite_enabled ) {
				$rewrite_args['with_front'] = false;
			}

			// WordPress defaults to value of `$args['has_archive']`.
			if ( isset( $rewrite['feeds'] ) && (bool) $rewrite['feeds'] !== $has_archive && $rewrite_enabled ) {
				$rewrite_args['feeds'] = (bool) $rewrite['feeds'];
			}

			// WordPress defaults to true.
			if ( isset( $rewrite['pages'] ) && ! $rewrite['pages'] && $rewrite_enabled ) {
				$rewrite_args['pages'] = false;
			}

			// Assemble rewrite args.
			if ( ! empty( $rewrite_args ) ) {
				$args['rewrite'] = $rewrite_args;
			} elseif ( ! $rewrite_enabled ) {
				$args['rewrite'] = false;
			}

			// WordPress and ACF default to $post_type key, a boolean can also be used.
			$query_var = (string) $post['query_var'];
			if ( 'custom_query_var' === $query_var ) {
				$query_var_name = (string) $post['query_var_name'];

				if ( ! empty( $query_var_name ) && $query_var_name !== $post['post_type'] ) {
					$args['query_var'] = $query_var_name;
				}
			} elseif ( 'none' === $query_var ) {
				$args['query_var'] = false;
			}

			// WordPress and ACF default to true.
			$can_export = (bool) $post['can_export'];
			if ( ! $can_export ) {
				$args['can_export'] = false;
			}

			// ACF defaults to false, while WordPress defaults to omitting (deletes only if author support is added).
			$args['delete_with_user'] = (bool) $post['delete_with_user'];

			return apply_filters( 'acf/post_type/registration_args', $args, $post );
		}

		/**
		 * Ensure the metabox being called does not perform any unsafe operations.
		 *
		 * @since 6.3.8
		 *
		 * @param WP_Post $post The post being rendered.
		 * @return mixed The callback result.
		 */
		public function build_safe_context_for_metabox_cb( $post ) {
			$post_types = $this->get_posts();
			$this_post  = array_filter(
				$post_types,
				function ( $post_type ) use ( $post ) {
					return $post_type['post_type'] === $post->post_type;
				}
			);
			if ( empty( $this_post ) || ! is_array( $this_post ) ) {
				// Unable to find the ACF post type. Don't do anything.
				return;
			}
			$acf_post_type = array_shift( $this_post );
			$original_cb   = isset( $acf_post_type['register_meta_box_cb'] ) ? $acf_post_type['register_meta_box_cb'] : false;

			// Prevent access to any wp_ prefixed functions in a callback.
			if ( apply_filters( 'acf/post_type/prevent_access_to_wp_functions_in_meta_box_cb', true ) && substr( strtolower( $original_cb ), 0, 3 ) === 'wp_' ) {
				// Don't execute register meta box callbacks if an internal wp function by default.
				return;
			}

			$unset     = array( '_POST', '_GET', '_REQUEST', '_COOKIE', '_SESSION', '_FILES', '_ENV', '_SERVER' );
			$originals = array();

			foreach ( $unset as $var ) {
				if ( isset( $GLOBALS[ $var ] ) ) {
					$originals[ $var ] = $GLOBALS[ $var ];
					$GLOBALS[ $var ]   = array(); //phpcs:ignore -- used for building a safe context
				}
			}

			$return = false;
			if ( is_callable( $original_cb ) ) {
				$return = call_user_func( $original_cb, $post );
			}

			foreach ( $unset as $var ) {
				if ( isset( $originals[ $var ] ) ) {
					$GLOBALS[ $var ] = $originals[ $var ]; //phpcs:ignore -- used for restoring the original context
				}
			}

			return $return;
		}

		/**
		 * Returns a string that can be used to create a post type in PHP.
		 *
		 * @since 6.1
		 *
		 * @param array $post The main post type array.
		 * @return string
		 */
		public function export_post_as_php( $post = array() ) {
			$return = '';
			if ( empty( $post ) ) {
				return $return;
			}

			$post_type_key = $post['post_type'];

			// Validate and prepare the post for export.
			$post = $this->validate_post( $post );
			$args = $this->get_post_type_args( $post, false );

			// Restore original metabox callback.
			if ( ! empty( $args['register_meta_box_cb'] ) && ! empty( $post['register_meta_box_cb'] ) ) {
				$args['register_meta_box_cb'] = (string) $post['register_meta_box_cb'];
			}

			$code = var_export( $args, true ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions -- Used for PHP export.

			if ( ! $code ) {
				return $return;
			}

			$code = $this->format_code_for_export( $code );

			$return .= "register_post_type( '{$post_type_key}', {$code} );\r\n";

			return esc_textarea( $return );
		}

		/**
		 * Flush rewrite rules whenever anything changes about a post type.
		 *
		 * @since 6.1
		 *
		 * @param array $post The main post type array.
		 */
		public function flush_post_cache( $post ) {
			// Bail early if we won't be able to register/unregister the post type.
			if ( empty( $post['post_type'] ) ) {
				return;
			}

			// Temporarily unregister the post type so that we can potentially re-register with the latest args below.
			if ( empty( $post['active'] ) || post_type_exists( $post['post_type'] ) ) {
				unregister_post_type( $post['post_type'] );
			}

			// When this is being called, the post type may not have been registered yet, so we do it now.
			if ( ! empty( $post['active'] ) ) {
				register_post_type( $post['post_type'], $this->get_post_type_args( $post ) );
			}

			// Flush our internal cache and the WordPress rewrite rules.
			parent::flush_post_cache( $post );
			flush_rewrite_rules();
		}

		/**
		 * Translates an ACF post.
		 *
		 * @since 6.1
		 *
		 * @param array $post The field group array.
		 * @return array
		 */
		public function translate_post( $post = array() ) {
			// Get settings.
			$l10n            = acf_get_setting( 'l10n' );
			$l10n_textdomain = acf_get_setting( 'l10n_textdomain' );

			// Translate field settings if textdomain is set.
			if ( $l10n && $l10n_textdomain ) {
				$post['title']       = acf_translate( $post['title'] );
				$post['description'] = acf_translate( $post['description'] );
				foreach ( $post['labels'] as $key => $label ) {
					$post['labels'][ $key ] = acf_translate( $label );
				}

				/**
				 * Filters the post array to translate strings.
				 *
				 * @date    12/02/2014
				 * @since   5.0.0
				 *
				 * @param   array $post The post array.
				 */
				$post = apply_filters( "acf/translate_{$this->hook_name}", $post );
			}

			return $post;
		}

		/**
		 * Prepares an ACF post type for import.
		 *
		 * @since 6.3.10
		 *
		 * @param array $post The ACF post array.
		 * @return array
		 */
		public function prepare_post_for_import( $post ) {
			if ( ! acf_get_setting( 'enable_meta_box_cb_edit' ) && ! empty( $post['register_meta_box_cb'] ) ) {
				$post['register_meta_box_cb'] = '';

				if ( ! empty( $post['ID'] ) ) {
					$existing_post = $this->get_post( $post['ID'] );

					if ( is_array( $existing_post ) ) {
						$post['register_meta_box_cb'] = ! empty( $existing_post['register_meta_box_cb'] ) ? (string) $existing_post['register_meta_box_cb'] : '';
					}
				}
			}

			return parent::prepare_post_for_import( $post );
		}

		/**
		 * Imports a post type from CPTUI.
		 *
		 * @since 6.1
		 *
		 * @param array $args Arguments from CPTUI.
		 * @return array
		 */
		public function import_cptui_post_type( $args ) {
			$acf_args = $this->get_settings_array();

			// Convert string boolean values to proper booleans.
			foreach ( $args as $key => $value ) {
				if ( in_array( $value, array( 'true', 'false' ), true ) ) {
					$args[ $key ] = filter_var( $value, FILTER_VALIDATE_BOOLEAN );
				}
			}

			if ( isset( $args['name'] ) ) {
				$acf_args['post_type'] = (string) $args['name'];
				unset( $args['name'] );
			}

			if ( isset( $args['labels'] ) ) {
				$acf_args['labels'] = array_merge( $acf_args['labels'], $args['labels'] );
				unset( $args['labels'] );
			}

			// ACF uses "name" as title, and stores in labels array.
			if ( isset( $args['label'] ) ) {
				$acf_args['title']          = (string) $args['label'];
				$acf_args['labels']['name'] = (string) $args['label'];
				unset( $args['label'] );
			}

			if ( isset( $args['singular_label'] ) ) {
				$acf_args['labels']['singular_name'] = (string) $args['singular_label'];
				unset( $args['singular_label'] );
			}

			if ( isset( $args['show_in_menu_string'] ) ) {
				$acf_args['admin_menu_parent'] = (string) $args['show_in_menu_string'];
				unset( $args['show_in_menu_string'] );
			}

			if ( isset( $args['rewrite'] ) ) {
				$rewrite = (bool) $args['rewrite'];

				if ( ! $rewrite ) {
					$acf_args['rewrite']['permalink_rewrite'] = 'no_permalink';
				} elseif ( ! empty( $args['rewrite_slug'] ) ) {
					$acf_args['rewrite']['permalink_rewrite'] = 'custom_permalink';
				} else {
					$acf_args['rewrite']['permalink_rewrite'] = 'post_type_key';
				}

				unset( $args['rewrite'] );
			}

			if ( isset( $args['rewrite_slug'] ) ) {
				$acf_args['rewrite']['slug'] = (string) $args['rewrite_slug'];
				unset( $args['rewrite_slug'] );
			}

			if ( isset( $args['rewrite_withfront'] ) ) {
				$acf_args['rewrite']['with_front'] = (bool) $args['rewrite_withfront'];
				unset( $args['rewrite_withfront'] );
			}

			// TODO: Investigate CPTUI usage of with_feeds, pages settings.
			// ACF handles capability type differently.
			if ( isset( $args['capability_type'] ) ) {
				if ( 'post' !== trim( $args['capability_type'] ) ) {
					$acf_args['rename_capabilities'] = true;

					$capabilities = explode( ',', $args['capability_type'] );
					$capabilities = array_map( 'trim', $capabilities );

					$acf_args['singular_capability_name'] = $capabilities[0];

					if ( count( $capabilities ) > 1 ) {
						$acf_args['plural_capability_name'] = $capabilities[1];
					}
				}

				unset( $args['capability_type'] );
			}

			// ACF names the has_archive slug differently.
			if ( isset( $args['has_archive_string'] ) ) {
				$acf_args['has_archive_slug'] = (string) $args['has_archive_string'];
				unset( $args['has_archive_string'] );
			}

			// ACF handles the query var and query var slug/name differently.
			if ( isset( $args['query_var'] ) ) {
				if ( ! $args['query_var'] ) {
					$acf_args['query_var'] = 'none';
				} elseif ( ! empty( $args['query_var_slug'] ) ) {
					$acf_args['query_var'] = 'custom_query_var';
				} else {
					$acf_args['query_var'] = 'post_type_key';
				}

				unset( $args['query_var'] );
			}

			if ( isset( $args['query_var_slug'] ) ) {
				$acf_args['query_var_name'] = (string) $args['query_var_slug'];
				unset( $args['query_var_slug'] );
			}

			$acf_args = wp_parse_args( $args, $acf_args );

			// ACF doesn't yet handle custom supports, so we're tacking onto the regular supports.
			if ( isset( $args['custom_supports'] ) ) {
				$custom_supports = explode( ',', (string) $args['custom_supports'] );
				$custom_supports = array_filter( array_map( 'trim', $custom_supports ) );

				if ( ! empty( $custom_supports ) ) {
					$acf_args['supports'] = array_merge( $acf_args['supports'], $custom_supports );
				}

				unset( $acf_args['custom_supports'] );
			}

			$acf_args['key']                    = uniqid( 'post_type_' );
			$acf_args['advanced_configuration'] = true;
			$acf_args['import_source']          = 'cptui';
			$acf_args['import_date']            = time();

			$existing_post_types = acf_get_acf_post_types();

			foreach ( $existing_post_types as $existing_post_type ) {
				// Post type already exists, so we need to update rather than import.
				if ( $acf_args['post_type'] === $existing_post_type['post_type'] ) {
					$acf_args        = $this->prepare_post_for_import( $acf_args );
					$acf_args['ID']  = $existing_post_type['ID'];
					$acf_args['key'] = $existing_post_type['key'];
					return $this->update_post( $acf_args );
				}
			}

			// Import the post normally.
			return $this->import_post( $acf_args );
		}
	}

}

acf_new_instance( 'ACF_Post_Type' );
