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 '

' . esc_html__( 'Custom post types', 'frontglut' ) . ' ' . esc_html__( 'Add new', 'frontglut' ) . '

'; echo ''; foreach ( $cpts as $slug => $d ) { echo ''; } if ( ! $cpts ) { echo ''; } echo '
' . esc_html__( 'Name', 'frontglut' ) . 'Slug' . esc_html__( 'Frontend', 'frontglut' ) . '
' . 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' ) . '
' . esc_html__( 'None yet.', 'frontglut' ) . '

' . esc_html__( 'Custom taxonomies', 'frontglut' ) . ' ' . esc_html__( 'Add new', 'frontglut' ) . '

'; foreach ( $taxes as $slug => $t ) { echo ''; } if ( ! $taxes ) { echo ''; } echo '
' . esc_html__( 'Name', 'frontglut' ) . 'Slug' . esc_html__( 'Type', 'frontglut' ) . '' . esc_html__( 'Attached to', 'frontglut' ) . '
' . 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' ) . '
' . esc_html__( 'None yet.', 'frontglut' ) . '
'; // 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 ''; } echo '
' . esc_html( $pt->label ) . ' ' . esc_html( $pt->name ) . '' . esc_html( $on ? __( 'Disable', 'frontglut' ) : __( 'Enable', 'frontglut' ) ) . '

' . esc_html__( 'Export / import definitions', 'frontglut' ) . '

' . esc_html__( 'Export JSON', 'frontglut' ) . '

'; wp_nonce_field( 'frontglut_pt_import' ); echo ''; submit_button( __( 'Import', 'frontglut' ), 'secondary' ); echo '
'; } 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' ) . '

'; wp_nonce_field( 'frontglut_cpt_save' ); echo ''; echo '

'; echo '

'; echo '

'; echo '

' . esc_html__( 'Supports', 'frontglut' ) . '
'; foreach ( array( 'title' => 'Title', 'editor' => 'Editor', 'thumbnail' => 'Featured image', 'excerpt' => 'Excerpt', 'comments' => 'Comments', 'author' => 'Author', 'custom-fields' => 'Custom fields' ) as $k => $l ) { echo ''; } echo '

'; foreach ( array( 'public' => 'Public', 'archive' => 'Has archive', 'rest' => 'Show in REST' ) as $k => $l ) { echo ''; } echo '

' . esc_html__( 'Taxonomies', 'frontglut' ) . '
'; foreach ( get_taxonomies( array( 'public' => true ), 'objects' ) as $t ) { echo ''; } echo '

' . esc_html__( 'Frontend access', 'frontglut' ) . '

' . esc_html__( 'Roles that can submit & edit their own', 'frontglut' ) . '' . wp_kses( $this->role_checks( 'submit_roles', $d['submit_roles'] ), \FrontGlut\Support\Ui::allowed_html() ) . '

'; echo '

' . esc_html__( 'Roles that can edit everyone\'s', 'frontglut' ) . '' . wp_kses( $this->role_checks( 'edit_all_roles', $d['edit_all_roles'] ), \FrontGlut\Support\Ui::allowed_html() ) . '

' . esc_html__( 'Roles whose items publish immediately (others go to review)', 'frontglut' ) . '' . wp_kses( $this->role_checks( 'publish_roles', $d['publish_roles'] ), \FrontGlut\Support\Ui::allowed_html() ) . '

'; echo '

'; submit_button(); echo '
'; } 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' ) . '

'; wp_nonce_field( 'frontglut_tax_save' ); echo '

'; echo '

'; echo '

 

' . esc_html__( 'Attach to', 'frontglut' ) . '
'; foreach ( get_post_types( array( 'public' => true ), 'objects' ) as $pt ) { if ( 'attachment' !== $pt->name ) { echo ''; } } echo '

'; submit_button(); echo '
'; } 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' ) ); } }