(Edit: going for the "Student" badge, and wondering if anyone out there could up vote this question?)
I answered a question about capabilities, but now I'm in need of help on the subject. And after reviewing the manual, I'm even more confused.
I have two custom post types both fully accessible to Administrators. I have Subscribers, some having access to one of these CPT's which is a "child" of the first and stores it's parent's ID in its _adm_id
metadata. These "special" Subscribers have access to the parent CPT admin table so they can click a link to create child CPT posts of parents with a special status. Next, the Subscriber is allowed to edit child posts (both its own and those created by others) but only if it is of a particular custom post status. Lastly, special Subscribers are not allowed to delete posts (or edit deleted posts), not even their own.
Here's what I've got (working code) ...
// Setup custom post types and statuses add_action('init', function() { // Custom Post Types register_post_type('adm-cpt', array( 'label' => __('Admin Only CPT'), 'show_ui' => TRUE, 'show_in_menu' => 'my-menu-item', 'show_in_admin_bar' => FALSE, 'capability_type' => 'adm', 'map_meta_cap' => TRUE, 'capabilities' => array( 'create_posts' => 'administrator', // Only admin can create, not special Subscribers ), )); register_post_type('sub-cpt', array( 'label' => __('Subscriber/Admin CPT'), 'show_ui' => TRUE, 'show_in_menu' => 'my-menu-item', 'show_in_admin_bar' => FALSE, 'capability_type' => 'sub', 'map_meta_cap' => TRUE, )); // Custom Post Statuses foreach(array( 'adm-childable' => __('Can Create Children'), 'sub-editable' => __('Any Subscriber Can Edit'), ) as $slug => $label) { register_post_status($slug, array( 'label' => _x($label, 'post'), 'label_count' => _n_noop($label .' <span class="count">(%s)</span>', $label .' <span class="count">(%s)</span>' ), 'public' => TRUE, )); } }); // Setup parent page in admin menu add_action('admin_menu', function() { // Add menu item if(current_user_can('administrator') || current_user_can('special-subscriber') ) { // Admin menu header add_menu_page( NULL, 'CPTs', 'exist', 'my-menu-item', '' ); } }); // Set up role add_action('wp_roles_init', function($wp_roles){ // Prepare $role = 'special-subscriber'; $caps = array( 'delete_subs' => FALSE, // No trashing ... 'delete_others_subs' => FALSE, 'delete_published_subs' => FALSE, 'delete_private_subs' => FALSE, 'edit_published_subs' => FALSE, // And no editing published/private posts ... 'edit_private_subs' => FALSE, 'edit_adms' => TRUE, // Allow viewing of adm-cpt table 'edit_posts' => TRUE, // WARNING: Here's the permission that is causing the problems! ); $name = __('"Special" Subscriber'); // Update role in database, if needed if($wp_roles->get_role($role) === NULL || $wp_roles->get_role($role)->capabilities != $caps || $wp_roles->roles[$role]['name'] !== $name ) { $wp_roles->remove_role($role); $wp_roles->add_role($role, $name, $caps); } }); // Dynamicly set capabilities add_action('user_has_cap', function($allcaps, $caps, $args, $user) { foreach($caps as $cap) { $perm = substr($cap, 0, strrpos($cap, '_')); $type = substr($cap, strlen($perm)+1); if(in_array($type, array('adm', 'adms')) && in_array('administrator', $user->roles) || in_array($type, array('sub', 'subs')) && !empty(array_intersect(array('administrator', 'special-subscriber'), $user->roles)) ) { // Check Subscriber if post is editable if(in_array($cap, array('edit_subs', 'edit_others_subs')) && in_array('special-subscriber', $user->roles) && !in_array('administrator', $user->roles) && !empty($args[2]) && ( !in_array(get_post_status($args[2]), array('sub-editable')) && !in_array($_REQUEST['original_post_status'], array('sub-editable', 'auto-draft')) // Creating || get_post_status(get_post_meta($args[2], '_adm_id', TRUE)) === 'trash' ) ) { $allcaps[$cap] = FALSE; } // Add the cap if(!isset($allcaps[$cap]) ) { $allcaps[$cap] = TRUE; // All the _adm and _sub capabilities are made available. } } } return $allcaps; }, 10, 4); // Add stuff to force proper navigation add_action('post_row_actions', function($actions, $post) { // Add link to adm-cpt table entries to create child if(get_post_type($post) === 'adm-cpt' && get_post_status($post) === 'adm-childable' && current_user_can('edit_subs') ) { $lbl = __('New '). get_post_type_object('sub-cpt')->labels->name; $actions['adm-cpt-create-sub-cpt'] = sprintf( '<a href="%s" aria-label="%s">%s</a>', admin_url('post-new.php?post_type=sub-cpt&adm_id='. $post->ID), esc_attr('“'. $lbl .'”'), $lbl ); } // Return return $actions; }, 10, 2); // Modify publish metabox add_action('post_submitbox_misc_actions', function($post) { $arr = array(); switch(get_post_type($post)) { case 'adm-cpt': $arr = array('adm-childable'); break; case 'sub-cpt': $arr = array('sub-editable'); break; default: return; } // Check that parent exists -- Should be in an init hook, but it's prettier here. if($_REQUEST['post_type'] === 'sub-cpt' && (empty($_REQUEST['adm_id']) || get_post_type($_REQUEST['adm_id']) !== 'adm-cpt') && (empty($post->_adm_id) || get_post_type($post->_adm_id) !== 'adm-cpt') ){ ?><script>window.document.location.replace("<?= admin_url('edit.php?post_type=adm-cpt') ?>")</script><?php return; } // Add custom post statuses ?><input type='hidden' name='adm_id' value='<?= $_REQUEST['adm_id'] ?>'><?php if(count($arr)) { ?><script> <?php foreach($arr as $k) { $obj = get_post_status_object($k); ?> jQuery("select#post_status").append("<option value=\"<?= $k ?>\"><?= $obj->label ?></option>"); <?php if(get_post_status($post) == $k) { ?> jQuery("#post-status-display").text("<?= $obj->label ?>"); jQuery("select#post_status").val("<?= $k ?>"); <?php } ?> <?php } ?> </script><?php } // Display parent -- Informational if(!empty($_REQUEST['adm_id']) || !empty($post->_adm_id) ) { $parent_id = $post->_adm_id; if(!$parent_id) $parent_id = $_REQUEST['adm_id']; ?><div class="misc-pub-section misc-pub-adm-cpt">Parent: <span id="post-status-display"><?= get_the_title($parent_id) ?></span></div><?php } }); // Save parent ID add_action('save_post_sub-cpt', function($post_id, $post, $update) { // Ensure we continue only id a new child is created if(defined('DOING_AUTOSAVE') && DOING_AUTOSAVE || get_post_type($post_id) !== 'sub-cpt' || empty($_REQUEST['adm_id']) || get_post_type($_REQUEST['adm_id']) !== 'adm-cpt' ) return; // Set parent ID update_post_meta($post_id, '_adm_id', $_REQUEST['adm_id']); }, 10, 3); // Navigation when changed to uneditable add_action('load-post.php', function(){ if(!empty($_REQUEST['post']) && get_post_type($_REQUEST['post']) === 'sub-cpt' && !current_user_can('edit_subs', $_REQUEST['post']) ) { delete_post_meta($_REQUEST['post'], '_edit_lock'); wp_redirect('edit.php?post_type=sub-cpt'); die(); } });
This issue here is that the special Subscriber is able to edit regular Posts and Comments. I understand this comes from the edit_posts
capability, and that capability allows for the editing/creation of all post types. However, removing it prevents special Subscribers from being able to create sub-cpt
posts, and granting edit_subs
does not solve the problem. Neither does setting the capabilities->create_post=special-subscriber
when registering the child CPT. I've been able to limit the ability of Subscribers from being able to create adm-cpt
posts by defining the capabilities
parameter when registering the post type. But I don't want special Subscribers to be able to edit/create any other posts other than those of the sub-cpt
type, and I can't seem to figure out how.
I've found a Q&A related to the subject, but this doesn't seem to work. The CPT's are mapped to custom capabilities, they exist, and the user_has_cap
filter dynamically grants each of these capabilities. I've even tried expressly defining them in the special-subscriber
role definition. Anyways, I'm sure the change is simple--what is it?
(If you're interested, I have another capability problem. When a special Subscriber sets the child CPT post_status
to publish, the post is locked and they are forwarded to edit.php
but I want the post to unlock and for the viewer to be forwarded to edit.php?post_type=sub-cpt
just like is done in the load-post.php
hook of my code, and I can't seem to figure out how.)
UPDATE: I've isolated it down to the placement of the CPT in the menu. When the CPT is registered as showing the UI using the register_post_type
option of show_in_menu=TRUE
, everything works as expected. But, when the CPT is added as a submenu of an old-fashioned admin menu item, things break. Adding the UI and hiding it results in the same problems, along with adding a subpage and redirecting it to the UI of the CPT. Examples:
// 1.) Works as expected if user has every custom capability add_action('init', function() { register_post_type('sub-cpt', array( 'label' => __('Subscriber/Admin CPT'), 'show_ui' => TRUE, 'show_in_menu' => TRUE, // Take note of this 'show_in_admin_bar' => FALSE, 'capability_type' => 'sub', 'map_meta_cap' => TRUE, )); } // 2.) Same as #1 with the exception that access to 'post-new.php' when "Add New" button is clicked is prohibited add_action('init', function() { register_post_type('sub-cpt', array( 'label' => __('Subscriber/Admin CPT'), 'show_ui' => TRUE, 'show_in_menu' => 'my-menu-item', // Take note of this 'show_in_admin_bar' => FALSE, 'capability_type' => 'sub', 'map_meta_cap' => TRUE, )); } add_action('admin_menu', function() { add_menu_page( 'CPT in title bar', 'CPT in menu', 'edit_subs', 'my-menu-item', '' ); } // 3.) Breaks the same as #2 add_action('init', function() { register_post_type('sub-cpt', array( 'label' => __('Subscriber/Admin CPT'), 'show_ui' => TRUE, 'show_in_menu' => FALSE, // Take note of this 'show_in_admin_bar' => FALSE, 'capability_type' => 'sub', 'map_meta_cap' => TRUE, )); } add_action('admin_menu', function() { global $submenu; add_menu_page( 'CPT in title bar', 'CPT in menu', 'edit_subs', 'my-menu-item' ); add_submenu_page( 'my-menu-item', get_post_type_object('sub-cpt')->label, get_post_type_object('sub-cpt')->label, 'edit_subs', 'my-menu-item-sub' ); // Change link $url = 'edit.php?post_type=sub-cpt'; $submenu['my-menu-item'][1][2] = admin_url($url); // Set URL to view CPT unset($submenu['my-menu-item'][0]); // Remove WP generated menu item });
If, I can get the "Add New" functionality to work with the CPT as a subpage, I think my problem will be solved because the edit_posts
capability giving me trouble can be specifically mapped to edit_subs
. Anyone know how to do this?