all_cpts()[ $pt ] ) ? 'cpt_' . $pt : $sec;
}, 10, 2 );
}
/* ---------------- data ---------------- */
public function all_cpts() {
$d = get_option( self::OPT_CPT, array() );
return is_array( $d ) ? $d : array();
}
public function all_taxes() {
$d = get_option( self::OPT_TAX, array() );
return is_array( $d ) ? $d : array();
}
public static function reserved() {
return array( 'post', 'page', 'attachment', 'revision', 'nav_menu_item', 'action', 'author', 'order', 'theme', 'category', 'post_tag', 'product', 'product_cat', 'product_tag', 'frontglut_entry', 'custom_css', 'customize_changeset', 'oembed_cache', 'user_request', 'wp_block', 'link_category', 'post_format' );
}
public function register() {
foreach ( $this->all_cpts() as $slug => $d ) {
if ( ! empty( $d['external'] ) || post_type_exists( $slug ) ) {
continue;
}
$sing = $d['singular'];
$plur = $d['plural'];
register_post_type( $slug, array(
'labels' => array( 'name' => $plur, 'singular_name' => $sing, 'add_new_item' => 'Add ' . $sing, 'edit_item' => 'Edit ' . $sing, 'all_items' => 'All ' . $plur ),
'public' => ! empty( $d['public'] ),
'has_archive' => ! empty( $d['archive'] ),
'show_in_rest' => ! empty( $d['rest'] ),
'menu_icon' => ! empty( $d['icon'] ) ? $d['icon'] : 'dashicons-admin-post',
'menu_position' => ! empty( $d['menu_order'] ) ? (int) $d['menu_order'] : 25,
'supports' => ! empty( $d['supports'] ) ? array_values( $d['supports'] ) : array( 'title', 'editor' ),
'rewrite' => array( 'slug' => ! empty( $d['rewrite'] ) ? $d['rewrite'] : $slug ),
'taxonomies' => ! empty( $d['taxonomies'] ) ? array_values( $d['taxonomies'] ) : array(),
) );
}
foreach ( $this->all_taxes() as $slug => $t ) {
if ( taxonomy_exists( $slug ) ) {
continue;
}
register_taxonomy( $slug, ! empty( $t['post_types'] ) ? array_values( $t['post_types'] ) : array(), array(
'labels' => array( 'name' => $t['plural'], 'singular_name' => $t['singular'] ),
'hierarchical' => ! empty( $t['hierarchical'] ),
'public' => true,
'show_in_rest' => ! empty( $t['rest'] ),
'show_admin_column' => true,
'rewrite' => array( 'slug' => $slug ),
'default_term' => ! empty( $t['default_term'] ) ? array( 'name' => $t['default_term'] ) : null,
) );
}
}
public function managed( $types ) {
return array_values( array_unique( array_merge( $types, array_keys( $this->all_cpts() ) ) ) );
}
public function menu( $items ) {
$posts = Plugin::instance()->modules()->get( 'posts' );
if ( ! $posts ) {
return $items;
}
$o = 20;
foreach ( $this->all_cpts() as $slug => $d ) {
if ( ! post_type_exists( $slug ) ) {
continue;
}
$o++;
$items[ 'cpt_' . $slug ] = array(
'label' => $d['plural'],
'icon' => ! empty( $d['dash_icon'] ) ? $d['dash_icon'] : 'category',
'group' => 'content',
'order' => $o,
'roles' => array(),
'render' => function () use ( $posts, $slug, $d ) {
if ( ! $this->user_in( 'submit_roles', $d ) && ! $this->user_in( 'edit_all_roles', $d ) && ! current_user_can( 'manage_options' ) ) {
return Ui::notice( esc_html__( 'Your role cannot manage this content type.', 'frontglut' ), 'warn' );
}
return $posts->manage_screen( 'cpt_' . $slug, $slug, $d['plural'], (int) ( isset( $d['form_id'] ) ? $d['form_id'] : 0 ) );
},
);
}
return $items;
}
private function user_in( $key, array $d, $user_id = 0 ) {
$u = $user_id ? get_userdata( $user_id ) : wp_get_current_user();
return $u && ! empty( $d[ $key ] ) && array_intersect( (array) $d[ $key ], (array) $u->roles );
}
/* ---------------- access hooks ---------------- */
public function can_edit( $ok, $post, $user_id ) {
$cpts = $this->all_cpts();
if ( ! isset( $cpts[ $post->post_type ] ) ) {
return $ok;
}
$d = $cpts[ $post->post_type ];
if ( $this->user_in( 'edit_all_roles', $d, $user_id ) || user_can( $user_id, 'manage_options' ) ) {
return true;
}
return $this->user_in( 'submit_roles', $d, $user_id ) && (int) $post->post_author === (int) $user_id && in_array( $post->post_status, (array) Settings::get( 'posts.edit_statuses', array( 'draft', 'pending', 'publish' ) ), true );
}
public function can_submit( $ok, $ptype, $logged, $form ) {
$cpts = $this->all_cpts();
if ( ! isset( $cpts[ $ptype ] ) ) {
return $ok;
}
if ( ! $logged ) {
return ! empty( $form['settings']['guest'] );
}
return $this->user_in( 'submit_roles', $cpts[ $ptype ] ) || $this->user_in( 'edit_all_roles', $cpts[ $ptype ] ) || current_user_can( 'manage_options' );
}
public function status( $status, $form, $uid, $ptype ) {
$cpts = $this->all_cpts();
if ( $uid && isset( $cpts[ $ptype ] ) ) {
if ( $this->user_in( 'publish_roles', $cpts[ $ptype ], $uid ) || user_can( $uid, 'manage_options' ) ) {
return 'publish';
}
return 'publish' === $status ? 'pending' : $status; // this type has its own publish-role list.
}
return $status;
}
/* ---------------- admin UI ---------------- */
public function tabs( $t ) {
$t['posttypes'] = array( 'label' => __( 'Post types', 'frontglut' ), 'order' => 13, 'cb' => array( $this, 'tab' ) );
return $t;
}
private function url( $action, $args = array() ) {
return wp_nonce_url( add_query_arg( array_merge( array( 'action' => $action ), $args ), admin_url( 'admin-post.php' ) ), $action );
}
public function tab() {
$cpts = $this->all_cpts();
$taxes = $this->all_taxes();
$edit = Request::route( 'cpt', 'key', '' );
$tedit = Request::route( 'tax', 'key', '' );
if ( 'new' === $edit || isset( $cpts[ $edit ] ) ) {
return $this->cpt_form( 'new' === $edit ? array() : $cpts[ $edit ], 'new' === $edit ? '' : $edit );
}
if ( 'new' === $tedit || isset( $taxes[ $tedit ] ) ) {
return $this->tax_form( 'new' === $tedit ? array() : $taxes[ $tedit ], 'new' === $tedit ? '' : $tedit );
}
echo '
';
echo '| ' . esc_html__( 'Name', 'frontglut' ) . ' | Slug | ' . esc_html__( 'Frontend', 'frontglut' ) . ' | |
';
foreach ( $cpts as $slug => $d ) {
echo '| ' . esc_html( $d['plural'] ) . '' . ( ! empty( $d['external'] ) ? ' (external)' : '' ) . ' | ' . esc_html( $slug ) . ' | ' . ( ! empty( $d['form_id'] ) ? 'Form #' . (int) $d['form_id'] : '—' ) . ' | ';
if ( empty( $d['external'] ) ) {
echo '' . esc_html__( 'Edit', 'frontglut' ) . ' | ';
}
echo '' . esc_html__( 'Remove', 'frontglut' ) . ' |
';
}
if ( ! $cpts ) {
echo '| ' . esc_html__( 'None yet.', 'frontglut' ) . ' |
';
}
echo '
| ' . esc_html__( 'Name', 'frontglut' ) . ' | Slug | ' . esc_html__( 'Type', 'frontglut' ) . ' | ' . esc_html__( 'Attached to', 'frontglut' ) . ' | |
';
foreach ( $taxes as $slug => $t ) {
echo '| ' . esc_html( $t['plural'] ) . ' | ' . esc_html( $slug ) . ' | ' . ( ! empty( $t['hierarchical'] ) ? 'Hierarchical' : 'Flat' ) . ' | ' . esc_html( implode( ', ', (array) ( isset( $t['post_types'] ) ? $t['post_types'] : array() ) ) ) . ' | ' . esc_html__( 'Edit', 'frontglut' ) . ' | ' . esc_html__( 'Remove', 'frontglut' ) . ' |
';
}
if ( ! $taxes ) {
echo '| ' . esc_html__( 'None yet.', 'frontglut' ) . ' |
';
}
echo '
';
// External.
echo '' . esc_html__( 'Manage existing post types from the frontend', 'frontglut' ) . '
' . esc_html__( 'Post types registered by your theme or other plugins. Enabling adds a section to the dashboard.', 'frontglut' ) . '
';
foreach ( get_post_types( array( 'show_ui' => true, '_builtin' => false ), 'objects' ) as $pt ) {
if ( isset( $cpts[ $pt->name ] ) && empty( $cpts[ $pt->name ]['external'] ) || in_array( $pt->name, self::reserved(), true ) || in_array( $pt->name, array( 'shop_order', 'shop_coupon', 'shop_order_refund', 'wp_template', 'wp_template_part', 'wp_navigation', 'wp_global_styles' ), true ) ) {
continue;
}
$on = isset( $cpts[ $pt->name ] ) && ! empty( $cpts[ $pt->name ]['external'] );
echo '' . esc_html( $pt->label ) . ' ' . esc_html( $pt->name ) . ' | ' . esc_html( $on ? __( 'Disable', 'frontglut' ) : __( 'Enable', 'frontglut' ) ) . ' |
';
}
echo '
' . esc_html__( 'Export / import definitions', 'frontglut' ) . '
' . esc_html__( 'Export JSON', 'frontglut' ) . '
';
}
private function role_checks( $name, $selected ) {
$o = '';
foreach ( wp_roles()->roles as $slug => $r ) {
$o .= ' ';
}
return $o . '
';
}
private function cpt_form( array $d, $slug ) {
$d = array_merge( array( 'plural' => '', 'singular' => '', 'icon' => 'dashicons-admin-post', 'dash_icon' => 'category', 'menu_order' => 25, 'supports' => array( 'title', 'editor', 'thumbnail' ), 'public' => 1, 'archive' => 1, 'rest' => 1, 'rewrite' => '', 'taxonomies' => array(), 'submit_roles' => array( 'administrator' ), 'edit_all_roles' => array( 'administrator' ), 'publish_roles' => array( 'administrator' ), 'form_id' => 0 ), $d );
echo '← ' . esc_html__( 'Back', 'frontglut' ) . '
';
}
private function tax_form( array $t, $slug ) {
$t = array_merge( array( 'plural' => '', 'singular' => '', 'hierarchical' => 1, 'post_types' => array(), 'rest' => 1, 'default_term' => '' ), $t );
echo '← ' . esc_html__( 'Back', 'frontglut' ) . '
';
}
private function guard() {
if ( ! current_user_can( 'frontglut_manage_settings' ) && ! current_user_can( 'manage_options' ) ) {
wp_die( 'Forbidden', 403 );
}
}
private function back( $msg ) {
wp_safe_redirect( Admin::url( 'posttypes', Admin::flash( $msg ) ) );
exit;
}
/* ---------------- save / delete ---------------- */
public static function clean_cpt( array $in, $slug ) {
$roles = array_keys( wp_roles()->roles );
$pick = function ( $k ) use ( $in, $roles ) {
return isset( $in[ $k ] ) ? array_values( array_intersect( array_map( 'sanitize_key', (array) $in[ $k ] ), $roles ) ) : array();
};
$sup = array( 'title', 'editor', 'thumbnail', 'excerpt', 'comments', 'author', 'custom-fields' );
return array(
'plural' => sanitize_text_field( $in['plural'] ?? $slug ),
'singular' => sanitize_text_field( $in['singular'] ?? $slug ),
'icon' => preg_match( '/^dashicons-[a-z0-9\-]+$/', $in['icon'] ?? '' ) ? $in['icon'] : 'dashicons-admin-post',
'dash_icon' => sanitize_html_class( $in['dash_icon'] ?? 'category' ),
'menu_order' => (int) ( $in['menu_order'] ?? 25 ),
'supports' => array_values( array_intersect( (array) ( $in['supports'] ?? array( 'title', 'editor' ) ), $sup ) ),
'public' => ! empty( $in['public'] ) ? 1 : 0,
'archive' => ! empty( $in['archive'] ) ? 1 : 0,
'rest' => ! empty( $in['rest'] ) ? 1 : 0,
'rewrite' => sanitize_title( $in['rewrite'] ?? '' ),
'taxonomies' => array_values( array_filter( array_map( 'sanitize_key', (array) ( $in['taxonomies'] ?? array() ) ), 'taxonomy_exists' ) ),
'submit_roles' => $pick( 'submit_roles' ),
'edit_all_roles' => $pick( 'edit_all_roles' ),
'publish_roles' => $pick( 'publish_roles' ),
'form_id' => (int) ( $in['form_id'] ?? 0 ),
);
}
public function save_cpt() {
check_admin_referer( 'frontglut_cpt_save' );
$this->guard();
$slug = sanitize_key( wp_unslash( $_POST['slug'] ?? '' ) );
$orig = sanitize_key( wp_unslash( $_POST['orig'] ?? '' ) );
if ( ! $slug || strlen( $slug ) > 20 || ( ! $orig && ( in_array( $slug, self::reserved(), true ) || post_type_exists( $slug ) ) ) ) {
$this->back( __( 'Invalid or reserved slug.', 'frontglut' ) );
}
$all = $this->all_cpts();
$data = self::clean_cpt( \FrontGlut\Support\Sanitize::deep( wp_unslash( $_POST ) ), $slug ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitised recursively here and validated per key by clean_cpt().
if ( ! $data['form_id'] ) {
$data['form_id'] = $this->make_form( $slug, $data );
}
$all[ $slug ] = $data;
update_option( self::OPT_CPT, $all, false );
update_option( 'frontglut_flush_rewrite', 1 );
Audit::log( 'cpt_saved', $slug );
$this->back( __( 'Post type saved.', 'frontglut' ) );
}
private function make_form( $slug, array $d ) {
$f = array();
if ( in_array( 'title', $d['supports'], true ) ) {
$f[] = Forms::f( 'title', 'text', __( 'Title', 'frontglut' ), array( 'required' => 1, 'storage' => 'post_title' ) );
}
if ( in_array( 'editor', $d['supports'], true ) ) {
$f[] = Forms::f( 'content', 'editor', __( 'Description', 'frontglut' ), array( 'storage' => 'post_content' ) );
}
if ( in_array( 'excerpt', $d['supports'], true ) ) {
$f[] = Forms::f( 'excerpt', 'textarea', __( 'Excerpt', 'frontglut' ), array( 'storage' => 'post_excerpt' ) );
}
if ( in_array( 'thumbnail', $d['supports'], true ) ) {
$f[] = Forms::f( 'featured', 'image', __( 'Featured image', 'frontglut' ), array( 'storage' => 'featured' ) );
}
foreach ( $d['taxonomies'] as $tax ) {
$to = get_taxonomy( $tax );
if ( $to ) {
$f[] = Forms::f( 'tax_' . $tax, 'taxonomy', $to->labels->name, array( 'storage' => 'tax', 'taxonomy' => $tax, 'ui' => $to->hierarchical ? 'checkboxes' : 'tags' ) );
}
}
return Forms::save( array( 'type' => 'post', 'title' => $d['plural'] . ' form', 'settings' => Forms::post_settings( array( 'post_type' => $slug, 'editor' => 'lite' ) ), 'fields' => $f ) );
}
public function delete_cpt() {
check_admin_referer( 'frontglut_cpt_delete' );
$this->guard();
$slug = sanitize_key( $_GET['slug'] ?? '' );
$all = $this->all_cpts();
unset( $all[ $slug ] );
update_option( self::OPT_CPT, $all, false );
update_option( 'frontglut_flush_rewrite', 1 );
$this->back( __( 'Removed (existing content is kept in the database).', 'frontglut' ) );
}
public function save_tax() {
check_admin_referer( 'frontglut_tax_save' );
$this->guard();
$slug = sanitize_key( wp_unslash( $_POST['slug'] ?? '' ) );
$all = $this->all_taxes();
if ( ! $slug || strlen( $slug ) > 32 || ( ! isset( $all[ $slug ] ) && ( in_array( $slug, self::reserved(), true ) || taxonomy_exists( $slug ) ) ) ) {
$this->back( __( 'Invalid or reserved slug.', 'frontglut' ) );
}
$all[ $slug ] = array(
'plural' => sanitize_text_field( wp_unslash( $_POST['plural'] ?? $slug ) ),
'singular' => sanitize_text_field( wp_unslash( $_POST['singular'] ?? $slug ) ),
'hierarchical' => ! empty( $_POST['hierarchical'] ) ? 1 : 0,
'rest' => ! empty( $_POST['rest'] ) ? 1 : 0,
'default_term' => sanitize_text_field( wp_unslash( $_POST['default_term'] ?? '' ) ),
'post_types' => array_values( array_filter( array_map( 'sanitize_key', (array) wp_unslash( $_POST['post_types'] ?? array() ) ), 'post_type_exists' ) ),
);
update_option( self::OPT_TAX, $all, false );
update_option( 'frontglut_flush_rewrite', 1 );
$this->back( __( 'Taxonomy saved.', 'frontglut' ) );
}
public function delete_tax() {
check_admin_referer( 'frontglut_tax_delete' );
$this->guard();
$all = $this->all_taxes();
unset( $all[ sanitize_key( $_GET['slug'] ?? '' ) ] );
update_option( self::OPT_TAX, $all, false );
$this->back( __( 'Removed.', 'frontglut' ) );
}
public function toggle_external() {
check_admin_referer( 'frontglut_pt_external' );
$this->guard();
$slug = sanitize_key( $_GET['slug'] ?? '' );
$all = $this->all_cpts();
if ( ! empty( $_GET['on'] ) && post_type_exists( $slug ) && ! in_array( $slug, self::reserved(), true ) ) {
$o = get_post_type_object( $slug );
$d = self::clean_cpt( array( 'plural' => $o->label, 'singular' => $o->labels->singular_name, 'supports' => array_keys( array_filter( array( 'title' => post_type_supports( $slug, 'title' ), 'editor' => post_type_supports( $slug, 'editor' ), 'thumbnail' => post_type_supports( $slug, 'thumbnail' ), 'excerpt' => post_type_supports( $slug, 'excerpt' ) ) ) ), 'submit_roles' => array( 'administrator', 'editor', 'author' ), 'edit_all_roles' => array( 'administrator', 'editor' ), 'publish_roles' => array( 'administrator', 'editor' ), 'taxonomies' => get_object_taxonomies( $slug ) ), $slug );
$d['external'] = 1;
$d['form_id'] = $this->make_form( $slug, $d );
$all[ $slug ] = $d;
} else {
unset( $all[ $slug ] );
}
update_option( self::OPT_CPT, $all, false );
$this->back( __( 'Updated.', 'frontglut' ) );
}
public function export() {
check_admin_referer( 'frontglut_pt_export' );
$this->guard();
nocache_headers();
header( 'Content-Type: application/json' );
header( 'Content-Disposition: attachment; filename="frontglut-post-types.json"' );
echo wp_json_encode( array( 'frontglut_pt' => 1, 'post_types' => $this->all_cpts(), 'taxonomies' => $this->all_taxes() ), JSON_PRETTY_PRINT );
exit;
}
public function import() {
check_admin_referer( 'frontglut_pt_import' );
$this->guard();
$d = \FrontGlut\Support\Sanitize::post_json( 'json' ); // Decoded and sanitised; clean_cpt() then validates every value.
if ( ! is_array( $d ) || empty( $d['frontglut_pt'] ) ) {
$this->back( __( 'Invalid JSON.', 'frontglut' ) );
}
$cpts = $this->all_cpts();
foreach ( (array) ( $d['post_types'] ?? array() ) as $slug => $def ) {
$slug = sanitize_key( $slug );
if ( $slug && strlen( $slug ) <= 20 && ! in_array( $slug, self::reserved(), true ) && is_array( $def ) ) {
$c = self::clean_cpt( $def, $slug );
$c['form_id'] = isset( $cpts[ $slug ]['form_id'] ) ? $cpts[ $slug ]['form_id'] : $this->make_form( $slug, $c );
$cpts[ $slug ] = $c;
}
}
update_option( self::OPT_CPT, $cpts, false );
$tx = $this->all_taxes();
foreach ( (array) ( $d['taxonomies'] ?? array() ) as $slug => $t ) {
$slug = sanitize_key( $slug );
if ( $slug && strlen( $slug ) <= 32 && ! in_array( $slug, self::reserved(), true ) && is_array( $t ) ) {
$tx[ $slug ] = array( 'plural' => sanitize_text_field( $t['plural'] ?? $slug ), 'singular' => sanitize_text_field( $t['singular'] ?? $slug ), 'hierarchical' => ! empty( $t['hierarchical'] ) ? 1 : 0, 'rest' => ! empty( $t['rest'] ) ? 1 : 0, 'default_term' => sanitize_text_field( $t['default_term'] ?? '' ), 'post_types' => array_values( array_map( 'sanitize_key', (array) ( $t['post_types'] ?? array() ) ) ) );
}
}
update_option( self::OPT_TAX, $tx, false );
update_option( 'frontglut_flush_rewrite', 1 );
$this->back( __( 'Imported.', 'frontglut' ) );
}
}