post_type ); return $post_type && ! empty( $post_type->cap->edit_posts ) && current_user_can( $post_type->cap->edit_posts ); } /** * Return a uniform authorization error without disclosing internal * post contents or publication state. * * @return WP_Error */ protected function rest_access_denied() { return new WP_Error( 'rest_forbidden', __( 'Sorry, you are not allowed to access Stacklar data.', 'stacklar-forms' ), array( 'status' => rest_authorization_required_code() ) ); } /** * Require Stacklar management access before querying a collection so * record IDs and collection totals cannot be enumerated anonymously. * * @param WP_REST_Request $request REST request. * @return true|WP_Error */ public function get_items_permissions_check( $request ) { if ( ! $this->current_user_can_manage_post_type() ) { return $this->rest_access_denied(); } return parent::get_items_permissions_check( $request ); } /** * Require Stacklar management access before resolving a single item. * * @param WP_REST_Request $request REST request. * @return true|WP_Error */ public function get_item_permissions_check( $request ) { if ( ! $this->current_user_can_manage_post_type() ) { return $this->rest_access_denied(); } return parent::get_item_permissions_check( $request ); } /** * Do not let the default controller's published-post shortcut bypass * the Stacklar manager capability when collection items are prepared. * * @param WP_Post $post Post object. * @return bool */ public function check_read_permission( $post ) { return $this->current_user_can_manage_post_type() && parent::check_read_permission( $post ); } } } class Stacklar_CPT { const PRIVILEGED_APPROVER_META = '_stacklar_privileged_actions_approved_by'; const PRIVILEGED_HASH_META = '_stacklar_privileged_actions_approval_hash'; /** * Map every form-management operation to the configured administrator * capability. Reusing the existing capability avoids a role migration and * keeps explicitly configured form-manager roles working. * * @param string $capability Administrator capability. * @return array */ protected function get_admin_only_post_type_capabilities( $capability ) { $capability = sanitize_key( (string) $capability ); if ( '' === $capability ) { $capability = 'manage_options'; } return array( // Keep the three meta capabilities generated from capability_type. // WordPress maps them to the administrator-only primitive caps below // for a specific object ID. Reusing a primitive capability here can // make multilingual plugins mistake manage_options for delete_post. 'edit_posts' => $capability, 'edit_others_posts' => $capability, 'publish_posts' => $capability, 'read_private_posts' => $capability, 'delete_posts' => $capability, 'delete_private_posts' => $capability, 'delete_published_posts' => $capability, 'delete_others_posts' => $capability, 'edit_private_posts' => $capability, 'edit_published_posts' => $capability, 'create_posts' => $capability, ); } /** * Record which privileged user approved the exact executable form state. * The content/status fingerprint prevents an unprivileged or out-of-band * change from inheriting an earlier approval. * * @param int $post_id Form post ID. * @param \WP_Post $post Form post. * @param bool $update Whether this is an update. */ public function approve_privileged_actions_on_save( $post_id, $post, $update ) { unset( $update ); if ( ! $post instanceof WP_Post || 'stacklar' !== $post->post_type ) { return; } if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) ) { return; } if ( 'publish' !== $post->post_status ) { delete_post_meta( $post_id, self::PRIVILEGED_APPROVER_META ); delete_post_meta( $post_id, self::PRIVILEGED_HASH_META ); return; } $user_id = get_current_user_id(); if ( ! $user_id || ! self::user_can_manage_forms( $user_id ) || ! current_user_can( 'edit_post', $post_id ) ) { return; } update_post_meta( $post_id, self::PRIVILEGED_APPROVER_META, absint( $user_id ) ); update_post_meta( $post_id, self::PRIVILEGED_HASH_META, self::get_privileged_approval_hash( $post ) ); } /** * Check a specific user against the stable configured management policy. * * @param int $user_id User ID. * @return bool */ public static function user_can_manage_forms( $user_id ) { $user_id = absint( $user_id ); if ( ! $user_id ) { return false; } $capability = apply_filters( 'stacklar_admin_capability', 'manage_options' ); $capability = is_string( $capability ) ? sanitize_key( $capability ) : ''; if ( '' === $capability ) { $capability = 'manage_options'; } if ( user_can( $user_id, $capability ) ) { return true; } return 'manage_options' === $capability && user_can( $user_id, 'activate_plugins' ); } /** * @param \WP_Post $post Form post. * @return string */ protected static function get_privileged_approval_hash( WP_Post $post ) { return hash( 'sha256', $post->post_type . "\n" . $post->post_status . "\n" . $post->post_content ); } public function register() { add_filter('use_block_editor_for_post_type', array( $this, 'enable_block_editor_for_cpt' ), 10, 2 ); add_action( 'save_post_stacklar', array( $this, 'approve_privileged_actions_on_save' ), 999, 3 ); if ( is_admin() ) { add_action( 'admin_menu', array( $this, 'customize_forms_list_submenu' ), 98 ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_menu_icon_styles' ) ); } $form_admin_capability = function_exists( 'stacklar_get_admin_capability' ) ? stacklar_get_admin_capability() : 'manage_options'; $form_admin_caps = $this->get_admin_only_post_type_capabilities( $form_admin_capability ); $menu_icon_asset = 'assets/images/stacklar-menu-icon.png'; $menu_icon_path = STACKLAR_PATH . $menu_icon_asset; $menu_icon_url = STACKLAR_URL . $menu_icon_asset; if ( file_exists( $menu_icon_path ) ) { $menu_icon_url = add_query_arg( 'ver', (string) filemtime( $menu_icon_path ), $menu_icon_url ); } $labels = array( 'name' => __( 'Stacklar', 'stacklar-forms' ), 'singular_name' => __( 'Stacklar', 'stacklar-forms' ), 'add_new' => __( 'Add New Form', 'stacklar-forms' ), 'add_new_item' => __( 'Add New Form', 'stacklar-forms' ), 'edit_item' => __( 'Edit Form', 'stacklar-forms' ), 'menu_name' => __( 'Stacklar', 'stacklar-forms' ), ); $args = array( 'labels' => $labels, 'public' => false, 'show_ui' => true, 'show_in_menu' => true, 'menu_icon' => $menu_icon_url, 'supports' => array( 'title', 'editor', 'revisions' ), 'show_in_rest' => true, 'rest_controller_class' => 'Stacklar_Admin_REST_Posts_Controller', 'publicly_queryable' => true, 'exclude_from_search' => true, 'has_archive' => false, 'rewrite' => array( 'slug' => 'stacklar', 'with_front' => false ), 'map_meta_cap' => true, 'capability_type' => 'stacklar', 'capabilities' => $form_admin_caps, ); register_post_type( 'stacklar', $args ); add_filter( 'manage_edit-stacklar_columns', array( $this, 'add_shortcode_column' ) ); add_action( 'manage_stacklar_posts_custom_column', array( $this, 'render_shortcode_column' ), 10, 2 ); } public function enable_block_editor_for_cpt( $enabled, $post_type ) { if ( 'stacklar' === $post_type ) return true; return $enabled; } protected function get_templates_menu_parent_slug() { return 'edit.php?post_type=stacklar'; } public function customize_forms_list_submenu() { global $submenu; $parent_slug = $this->get_templates_menu_parent_slug(); if ( empty( $submenu[ $parent_slug ] ) || ! is_array( $submenu[ $parent_slug ] ) ) { return; } foreach ( $submenu[ $parent_slug ] as $index => $item ) { $item_slug = isset( $item[2] ) ? (string) $item[2] : ''; if ( $parent_slug === $item_slug ) { $submenu[ $parent_slug ][ $index ][0] = __( 'All Forms', 'stacklar-forms' ); break; } } } public function enqueue_menu_icon_styles() { wp_register_style( 'stacklar-menu-icon', false, array(), STACKLAR_VERSION ); wp_add_inline_style( 'stacklar-menu-icon', '#adminmenu .menu-icon-stacklar .wp-menu-image img{width:20px;height:20px;object-fit:contain;}' ); wp_enqueue_style( 'stacklar-menu-icon' ); } public function add_shortcode_column( $columns ) { $new = array(); foreach ( $columns as $key => $label ) { $new[ $key ] = $label; if ( 'title' === $key ) { $new['stacklar_shortcode'] = __( 'Shortcode', 'stacklar-forms' ); } } if ( ! isset( $new['stacklar_shortcode'] ) ) { $new['stacklar_shortcode'] = __( 'Shortcode', 'stacklar-forms' ); } return $new; } public function render_shortcode_column( $column, $post_id ) { if ( 'stacklar_shortcode' !== $column ) { return; } $shortcode = '[stacklar id="' . intval( $post_id ) . '"]'; echo '
'; echo '' . esc_html( $shortcode ) . ''; echo '
'; } }