$plural,
'singular_name' => $singular,
'menu_name' => $plural,
'name_admin_bar' => $singular,
'add_new' => 'Add New',
'add_new_item' => 'Add New ' . $singular,
'new_item' => 'New ' . $singular,
'edit_item' => 'Edit ' . $singular,
'view_item' => 'View ' . $singular,
'all_items' => 'All ' . $plural,
'search_items' => 'Search ' . $plural,
'parent_item_colon' => 'Parent ' . $plural . ':',
'not_found' => 'No ' . strtolower( $plural ) . ' found.',
'not_found_in_trash' => 'No ' . strtolower( $plural ) . ' found in Trash.',
);
$args = array(
'labels' => $labels,
'public' => ! empty( $cpt['public'] ),
'publicly_queryable' => ! empty( $cpt['public'] ),
'show_ui' => ! empty( $cpt['public'] ),
'show_in_menu' => ! empty( $cpt['public'] ),
'query_var' => true,
'rewrite' => array( 'slug' => $slug ),
'capability_type' => 'post',
'has_archive' => ! empty( $cpt['has_archive'] ),
'hierarchical' => false,
'menu_position' => null,
'menu_icon' => sanitize_text_field( $cpt['menu_icon'] ?? 'dashicons-admin-post' ),
'show_in_rest' => ! empty( $cpt['show_in_rest'] ),
'supports' => isset( $cpt['supports'] ) && is_array( $cpt['supports'] ) ? $cpt['supports'] : array( 'title', 'editor' ),
);
register_post_type( $slug, $args );
}
}
public function admin_menu() {
$hook = add_submenu_page(
'metaforge',
'Post Types',
'Post Types',
'manage_metaforge',
'metaforge_post_types',
array( $this, 'admin_page' )
);
add_action( "load-{$hook}", array( $this, 'load_screen' ) );
add_submenu_page(
null,
'Add Post Type',
'Add Post Type',
'manage_metaforge',
'metaforge_post_types_add',
'__return_null'
);
}
public function load_screen() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$action = isset( $_GET['action'] ) ? sanitize_key( wp_unslash( $_GET['action'] ) ) : '';
if ( $action !== 'add' && $action !== 'edit' ) {
require_once METAFORGE_PATH . 'admin/PostTypeListTable.php';
$this->list_table = new MetaForge_PostTypeListTable();
$screen = get_current_screen();
if ( $screen ) {
register_column_headers( $screen, $this->list_table->get_columns() );
}
}
}
public function admin_page() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only UI action parameter.
$action = isset( $_GET['action'] ) ? sanitize_key( wp_unslash( $_GET['action'] ) ) : '';
if ( $action === 'add' || $action === 'edit' ) {
$this->render_form();
return;
}
$this->render_list();
}
private function render_list() {
if ( isset( $this->list_table ) ) {
$list_table = $this->list_table;
} else {
require_once METAFORGE_PATH . 'admin/PostTypeListTable.php';
$list_table = new MetaForge_PostTypeListTable();
}
$list_table->prepare_items();
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$search_val = isset( $_REQUEST['s'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['s'] ) ) : '';
$is_empty = empty( $list_table->items ) && empty( $search_val );
?>
$item ) {
if ( ( $item['slug'] ?? '' ) === $search_slug ) {
$index = $i;
break;
}
}
} elseif ( isset( $_GET['index'] ) ) {
$index = absint( wp_unslash( $_GET['index'] ) );
}
$new_cpt = array(
'plural_label' => sanitize_text_field( wp_unslash( $_POST['plural_label'] ?? '' ) ),
'singular_label' => sanitize_text_field( wp_unslash( $_POST['singular_label'] ?? '' ) ),
'slug' => sanitize_key( wp_unslash( $_POST['slug'] ?? '' ) ),
'menu_icon' => sanitize_text_field( wp_unslash( $_POST['menu_icon'] ?? 'dashicons-admin-post' ) ),
'public' => isset( $_POST['public'] ) ? 1 : 0,
'has_archive' => isset( $_POST['has_archive'] ) ? 1 : 0,
'show_in_rest' => isset( $_POST['show_in_rest'] ) ? 1 : 0,
'supports' => isset( $_POST['supports'] ) && is_array( $_POST['supports'] ) ? array_map( 'sanitize_text_field', wp_unslash( $_POST['supports'] ) ) : array(),
);
$existing_slugs = array_column( $cpts, 'slug' );
if ( $index !== null && isset( $cpts[ $index ] ) ) {
$current_slug = $cpts[ $index ]['slug'];
$existing_slugs = array_diff( $existing_slugs, array( $current_slug ) );
} else {
$current_slug = '';
}
if ( empty( $new_cpt['plural_label'] ) ) {
$this->form_error = '⚠️ Plural label cannot be empty.';
} elseif ( empty( $new_cpt['singular_label'] ) ) {
$this->form_error = '⚠️ Singular label cannot be empty.';
} elseif ( empty( $new_cpt['slug'] ) ) {
$this->form_error = '⚠️ Slug cannot be empty.';
} elseif ( in_array( $new_cpt['slug'], $existing_slugs ) || ( $new_cpt['slug'] !== $current_slug && post_type_exists( $new_cpt['slug'] ) ) ) {
$this->form_error = '⚠️ A Post Type with this slug already exists. Please use a unique slug to prevent conflicts.';
}
if ( ! $this->form_error ) {
if ( $index !== null && isset( $cpts[ $index ] ) ) {
$cpts[ $index ] = $new_cpt;
} else {
$cpts[] = $new_cpt;
$index = array_key_last( $cpts );
}
update_option( 'metaforge_post_types', $cpts );
flush_rewrite_rules();
wp_safe_redirect( admin_url( 'admin.php?page=metaforge_post_types&action=edit&slug=' . rawurlencode( $new_cpt['slug'] ) . '&saved=true' ) );
exit;
}
}
}
private function render_form() {
$cpts = get_option( 'metaforge_post_types', array() );
$index = null;
$saved = false;
$error = $this->form_error;
// phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only UI parameters.
if ( isset( $_GET['slug'] ) && ! empty( $_GET['slug'] ) ) {
$search_slug = sanitize_key( wp_unslash( $_GET['slug'] ) );
foreach ( $cpts as $i => $item ) {
if ( ( $item['slug'] ?? '' ) === $search_slug ) {
$index = $i;
break;
}
}
} elseif ( isset( $_GET['index'] ) ) {
$index = intval( wp_unslash( $_GET['index'] ) );
}
$cpt = ( $index !== null && isset( $cpts[ $index ] ) ) ? $cpts[ $index ] : array();
// phpcs:enable WordPress.Security.NonceVerification.Recommended
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only notice query parameter.
if ( isset( $_GET['saved'] ) && sanitize_text_field( wp_unslash( $_GET['saved'] ) ) === 'true' ) {
$saved = true;
}
// phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce verified prior to setting form_error if submitted.
if ( $error && isset( $_POST['metaforge_save_cpt'] ) ) {
$plural = sanitize_text_field( wp_unslash( $_POST['plural_label'] ?? '' ) );
$singular = sanitize_text_field( wp_unslash( $_POST['singular_label'] ?? '' ) );
$slug = sanitize_key( wp_unslash( $_POST['slug'] ?? '' ) );
$icon = sanitize_text_field( wp_unslash( $_POST['menu_icon'] ?? 'dashicons-admin-post' ) );
$public = isset( $_POST['public'] ) ? 1 : 0;
$archive = isset( $_POST['has_archive'] ) ? 1 : 0;
$rest = isset( $_POST['show_in_rest'] ) ? 1 : 0;
$supports = isset( $_POST['supports'] ) && is_array( $_POST['supports'] ) ? array_map( 'sanitize_text_field', wp_unslash( $_POST['supports'] ) ) : array();
} else {
// phpcs:enable WordPress.Security.NonceVerification.Missing
$plural = $cpt['plural_label'] ?? '';
$singular = $cpt['singular_label'] ?? '';
$slug = $cpt['slug'] ?? '';
$icon = $cpt['menu_icon'] ?? 'dashicons-admin-post';
$public = $cpt['public'] ?? 1;
$archive = $cpt['has_archive'] ?? 0;
$rest = $cpt['show_in_rest'] ?? 1;
$supports = $cpt['supports'] ?? array( 'title', 'editor', 'thumbnail' );
}
$is_edit = $index !== null;
?>
'Unauthorized' ) );
}
$prompt = sanitize_text_field( wp_unslash( $_POST['prompt'] ?? '' ) );
if ( empty( $prompt ) ) {
wp_send_json_error( array( 'message' => 'Please provide a prompt.' ) );
}
$client = new MetaForge_APIClient();
if ( ! $client->is_ready() ) {
wp_send_json_error( array( 'message' => 'AI is not configured. Please set your API key in MetaForge -> AI Settings.' ) );
}
$system_prompt = "You are a WordPress developer assistant.
Your task is to generate custom post type registration configuration based on the user's prompt.
Respond ONLY with a valid raw JSON object. Do not include markdown blocks.
JSON format:
{
\"plural_label\": \"Projects\",
\"singular_label\": \"Project\",
\"slug\": \"project\",
\"menu_icon\": \"dashicons-portfolio\",
\"public\": 1,
\"has_archive\": 0,
\"show_in_rest\": 1,
\"supports\": [\"title\", \"editor\", \"thumbnail\", \"excerpt\"]
}
Use a valid dashicons class for menu_icon. In supports, choose from: title, editor, author, thumbnail, excerpt, trackbacks, custom-fields, comments, revisions, page-attributes. Note: has_archive must be 0 by default unless specifically requested in the prompt.";
$response = $client->generate( $system_prompt, 'Generate a CPT for: ' . $prompt, true );
if ( is_wp_error( $response ) ) {
wp_send_json_error( array( 'message' => $response->get_error_message() ) );
}
$response = trim( $response );
if ( strpos( $response, '```json' ) === 0 ) {
$response = trim( substr( $response, 7, -3 ) );
} elseif ( strpos( $response, '```' ) === 0 ) {
$response = trim( substr( $response, 3, -3 ) );
}
$decoded = json_decode( $response, true );
if ( ! $decoded || empty( $decoded['slug'] ) ) {
wp_send_json_error( array( 'message' => 'Failed to parse AI response.' ) );
}
$decoded = map_deep( $decoded, 'sanitize_text_field' );
// has_archive should be unchecked (0) by default unless specifically requested
if ( ! isset( $decoded['has_archive'] ) || ! preg_match( '/\b(archive|archives)\b/i', $prompt ) ) {
$decoded['has_archive'] = 0;
} else {
$decoded['has_archive'] = ! empty( $decoded['has_archive'] ) ? 1 : 0;
}
wp_send_json_success( $decoded );
}
public function delete_post_type_completely( $slug ) {
$slug = sanitize_key( $slug );
if ( empty( $slug ) ) {
return false;
}
// 1. Remove from metaforge_post_types option
$cpts = get_option( 'metaforge_post_types', array() );
$found = false;
foreach ( $cpts as $index => $cpt ) {
if ( isset( $cpt['slug'] ) && $cpt['slug'] === $slug ) {
unset( $cpts[ $index ] );
$found = true;
break;
}
}
if ( $found ) {
update_option( 'metaforge_post_types', array_values( $cpts ) );
}
// 2. Unregister post type if active in WP runtime
if ( post_type_exists( $slug ) ) {
unregister_post_type( $slug );
}
// 3. Remove post_type from registered taxonomies in metaforge_taxonomies option
$taxs = get_option( 'metaforge_taxonomies', array() );
$tax_updated = false;
foreach ( $taxs as &$tax ) {
if ( isset( $tax['post_types'] ) && is_array( $tax['post_types'] ) ) {
if ( in_array( $slug, $tax['post_types'], true ) ) {
$tax['post_types'] = array_values( array_diff( $tax['post_types'], array( $slug ) ) );
$tax_updated = true;
}
}
}
if ( $tax_updated ) {
update_option( 'metaforge_taxonomies', $taxs );
}
// 4. Remove post_type from metaforge_post_order_settings
$order_settings = get_option( 'metaforge_post_order_settings', array() );
if ( isset( $order_settings['enabled_post_types'] ) && is_array( $order_settings['enabled_post_types'] ) ) {
if ( in_array( $slug, $order_settings['enabled_post_types'], true ) ) {
$order_settings['enabled_post_types'] = array_values( array_diff( $order_settings['enabled_post_types'], array( $slug ) ) );
update_option( 'metaforge_post_order_settings', $order_settings );
}
}
// 5. Clean up Field Group location rules referencing this post_type
$field_groups = get_posts(
array(
'post_type' => 'metaforge_group',
'posts_per_page' => -1,
'post_status' => 'any',
)
);
foreach ( $field_groups as $fg ) {
$config = get_post_meta( $fg->ID, '_metaforge_group_config', true );
if ( is_array( $config ) && ! empty( $config['location_rules'] ) ) {
$rule_groups_changed = false;
$new_location_rules = array();
foreach ( $config['location_rules'] as $rule_group ) {
$new_rule_group = array();
foreach ( $rule_group as $rule ) {
if ( ( $rule['param'] ?? '' ) === 'post_type' && ( $rule['value'] ?? '' ) === $slug ) {
$rule_groups_changed = true;
} else {
$new_rule_group[] = $rule;
}
}
if ( ! empty( $new_rule_group ) ) {
$new_location_rules[] = $new_rule_group;
}
}
if ( $rule_groups_changed ) {
$config['location_rules'] = $new_location_rules;
update_post_meta( $fg->ID, '_metaforge_group_config', $config );
}
}
}
// 6. Clear field groups cache transient
delete_transient( 'metaforge_all_field_groups_cache' );
// 7. Flush rewrite rules
flush_rewrite_rules();
return $found;
}
public function ajax_delete() {
if ( ! check_ajax_referer( 'metaforge_delete_cpt_nonce', 'nonce', false ) && ! check_ajax_referer( 'metaforge_cpt_generation_nonce', 'nonce', false ) ) {
wp_send_json_error( array( 'message' => 'Security check failed. Please refresh the page and try again.' ) );
}
if ( ! current_user_can( 'manage_metaforge' ) ) {
wp_send_json_error( array( 'message' => 'Unauthorized' ) );
}
$slug = isset( $_POST['slug'] ) ? sanitize_key( $_POST['slug'] ) : '';
if ( empty( $slug ) ) {
wp_send_json_error( array( 'message' => 'Invalid post type slug.' ) );
}
$success = $this->delete_post_type_completely( $slug );
if ( $success ) {
wp_send_json_success();
}
wp_send_json_error( array( 'message' => 'Post type not found or could not be deleted.' ) );
}
}