| | 1 | <?php |
| | 2 | |
| | 3 | /** |
| | 4 | * @group plugins |
| | 5 | * @group admin |
| | 6 | */ |
| | 7 | class Tests_Admin_CPT_Admin_Page_Capabilities extends WP_UnitTestCase { |
| | 8 | |
| | 9 | /** |
| | 10 | * @ticket 22895 |
| | 11 | */ |
| | 12 | function test_user_can_access_admin_page_without_create_posts_capability() { |
| | 13 | |
| | 14 | $cpt_slug = 'test-cpt'; |
| | 15 | |
| | 16 | $original_user = get_current_user_id(); |
| | 17 | |
| | 18 | global $pagenow; |
| | 19 | |
| | 20 | //register post type (before menu/typenow stuff!) |
| | 21 | $this->_register_test_post_type( $cpt_slug ); |
| | 22 | |
| | 23 | //reset globals |
| | 24 | $_GET = $_POST = $_REQUEST = array(); |
| | 25 | |
| | 26 | //mock the CPT query |
| | 27 | $_GET['post_type'] = $_POST['post_type'] = $_REQUEST['post_type'] = $cpt_slug; |
| | 28 | |
| | 29 | //and mock the base page |
| | 30 | $GLOBALS['hook_suffix'] = $pagenow = 'edit.php'; |
| | 31 | |
| | 32 | //this will set $typenow to $_REQUEST['post_type'] |
| | 33 | //as long as the query value is a valid post type |
| | 34 | set_current_screen(); |
| | 35 | |
| | 36 | //subscriber is lowest level, and should work as a good base for adding caps |
| | 37 | wp_set_current_user( $this->factory->user->create( array( 'role' => 'subscriber' ) ) ); |
| | 38 | |
| | 39 | //mock menus/privs |
| | 40 | $this->_mock_post_menu(); |
| | 41 | $this->_mock_cpt_menu( $cpt_slug ); |
| | 42 | $this->_mock_menu_privs(); |
| | 43 | |
| | 44 | //grant only the plural edit cap |
| | 45 | //important part is *no* create cap |
| | 46 | $caps = array( |
| | 47 | "edit_{$cpt_slug}s" => true, |
| | 48 | ); |
| | 49 | |
| | 50 | $role = get_role( 'subscriber' ); |
| | 51 | |
| | 52 | if ( $role ) { |
| | 53 | foreach ( $caps as $cap => $grant ) { |
| | 54 | $role->add_cap( $cap, $grant ); |
| | 55 | } |
| | 56 | $this->assertTrue( user_can_access_admin_page() ); |
| | 57 | } |
| | 58 | |
| | 59 | wp_set_current_user( $original_user ); |
| | 60 | |
| | 61 | } |
| | 62 | |
| | 63 | /** |
| | 64 | * Register a custom post type for testing |
| | 65 | * |
| | 66 | * @param string $cpt_slug The slug with which to register the post type |
| | 67 | * @param array $args Args to override this func's default post type registration args |
| | 68 | */ |
| | 69 | private function _register_test_post_type( $cpt_slug, $args = array() ) { |
| | 70 | |
| | 71 | $args = wp_parse_args( $args, array( |
| | 72 | 'label' => 'Test CPT', |
| | 73 | 'show_in_menu' => true, |
| | 74 | 'public' => true, |
| | 75 | 'capabilities' => array( |
| | 76 | 'edit_post' => "edit_{$cpt_slug}", |
| | 77 | 'edit_posts' => "edit_{$cpt_slug}s", |
| | 78 | 'edit_others_posts' => "edit_others_{$cpt_slug}s", |
| | 79 | 'publish_posts' => "publish_{$cpt_slug}s", |
| | 80 | 'read_private_posts' => "read_private_{$cpt_slug}s", |
| | 81 | 'delete_posts' => "delete_{$cpt_slug}s", |
| | 82 | 'delete_private_posts' => "delete_private_{$cpt_slug}s", |
| | 83 | 'delete_published_posts' => "delete_published_{$cpt_slug}s", |
| | 84 | 'delete_others_posts' => "delete_others_{$cpt_slug}s", |
| | 85 | 'edit_private_posts' => "edit_private_{$cpt_slug}s", |
| | 86 | 'edit_published_posts' => "edit_published_{$cpt_slug}s", |
| | 87 | 'create_posts' => "edit_others_{$cpt_slug}s", |
| | 88 | ), |
| | 89 | 'map_meta_cap' => false, |
| | 90 | ) ); |
| | 91 | |
| | 92 | register_post_type( $cpt_slug, $args ); |
| | 93 | |
| | 94 | } |
| | 95 | |
| | 96 | /** |
| | 97 | * Mock the addition of the Post menu/submenus |
| | 98 | * |
| | 99 | * This is necessary in order to have 'edit.php' |
| | 100 | * present in the $_wp_menu_no_privs array, |
| | 101 | * which is what causes this scenario to occur. |
| | 102 | * Pulled from wp-admin/menu.php |
| | 103 | * |
| | 104 | * Differences: |
| | 105 | * Excluded addition of taxonomies as submenus |
| | 106 | */ |
| | 107 | private function _mock_post_menu() { |
| | 108 | global $menu, $submenu; |
| | 109 | $menu[5] = array( |
| | 110 | __( 'Posts' ), |
| | 111 | 'edit_posts', |
| | 112 | 'edit.php', |
| | 113 | '', |
| | 114 | 'open-if-no-js menu-top menu-icon-post', |
| | 115 | 'menu-posts', |
| | 116 | 'dashicons-admin-post' |
| | 117 | ); |
| | 118 | $submenu['edit.php'][5] = array( __( 'All Posts' ), 'edit_posts', 'edit.php' ); |
| | 119 | $submenu['edit.php'][10] = array( |
| | 120 | _x( 'Add New', 'post' ), |
| | 121 | get_post_type_object( 'post' )->cap->create_posts, |
| | 122 | 'post-new.php' |
| | 123 | ); |
| | 124 | } |
| | 125 | |
| | 126 | /** |
| | 127 | * Mock the addition of a menu/submenu for a CPT |
| | 128 | * |
| | 129 | * Pulled from wp-admin/menu.php |
| | 130 | * Differences: Outside of the get_post_types results loop |
| | 131 | * Not adding taxonomies as submenus (unneeded for this test) |
| | 132 | */ |
| | 133 | private function _mock_cpt_menu( $cpt_slug ) { |
| | 134 | global $menu, $submenu; |
| | 135 | |
| | 136 | //dupe so the diff between this and original file is minimal |
| | 137 | $ptype = $cpt_slug; |
| | 138 | |
| | 139 | $_wp_last_object_menu = 25; |
| | 140 | |
| | 141 | $ptype_obj = get_post_type_object( $ptype ); |
| | 142 | // Check if it should be a submenu. |
| | 143 | if ( $ptype_obj->show_in_menu !== true ) { |
| | 144 | return; |
| | 145 | } |
| | 146 | $ptype_menu_position = is_int( $ptype_obj->menu_position ) ? $ptype_obj->menu_position : ++ $_wp_last_object_menu; // If we're to use $_wp_last_object_menu, increment it first. |
| | 147 | $ptype_for_id = sanitize_html_class( $ptype ); |
| | 148 | |
| | 149 | if ( is_string( $ptype_obj->menu_icon ) ) { |
| | 150 | // Special handling for data:image/svg+xml and Dashicons. |
| | 151 | if ( 0 === strpos( $ptype_obj->menu_icon, 'data:image/svg+xml;base64,' ) || 0 === strpos( $ptype_obj->menu_icon, 'dashicons-' ) ) { |
| | 152 | $menu_icon = $ptype_obj->menu_icon; |
| | 153 | } else { |
| | 154 | $menu_icon = esc_url( $ptype_obj->menu_icon ); |
| | 155 | } |
| | 156 | $ptype_class = $ptype_for_id; |
| | 157 | } else { |
| | 158 | $menu_icon = 'dashicons-admin-post'; |
| | 159 | $ptype_class = 'post'; |
| | 160 | } |
| | 161 | |
| | 162 | /* |
| | 163 | * If $ptype_menu_position is already populated or will be populated |
| | 164 | * by a hard-coded value below, increment the position. |
| | 165 | */ |
| | 166 | $core_menu_positions = array( 59, 60, 65, 70, 75, 80, 85, 99 ); |
| | 167 | while ( isset( $menu[ $ptype_menu_position ] ) || in_array( $ptype_menu_position, $core_menu_positions ) ) { |
| | 168 | $ptype_menu_position ++; |
| | 169 | } |
| | 170 | |
| | 171 | $menu[ $ptype_menu_position ] = array( |
| | 172 | esc_attr( $ptype_obj->labels->menu_name ), |
| | 173 | $ptype_obj->cap->edit_posts, |
| | 174 | "edit.php?post_type=$ptype", |
| | 175 | '', |
| | 176 | 'menu-top menu-icon-' . $ptype_class, |
| | 177 | 'menu-posts-' . $ptype_for_id, |
| | 178 | $menu_icon |
| | 179 | ); |
| | 180 | $submenu["edit.php?post_type=$ptype"][5] = array( |
| | 181 | $ptype_obj->labels->all_items, |
| | 182 | $ptype_obj->cap->edit_posts, |
| | 183 | "edit.php?post_type=$ptype" |
| | 184 | ); |
| | 185 | $submenu["edit.php?post_type=$ptype"][10] = array( |
| | 186 | $ptype_obj->labels->add_new, |
| | 187 | $ptype_obj->cap->create_posts, |
| | 188 | "post-new.php?post_type=$ptype" |
| | 189 | ); |
| | 190 | } |
| | 191 | |
| | 192 | /** |
| | 193 | * Mock the building of priveleges for menus/submenus |
| | 194 | * |
| | 195 | * Pulled from wp-admin/includes/menu.php |
| | 196 | */ |
| | 197 | private function _mock_menu_privs() { |
| | 198 | global $menu, $submenu, $_wp_menu_nopriv, $_wp_submenu_nopriv; |
| | 199 | |
| | 200 | $_wp_submenu_nopriv = array(); |
| | 201 | $_wp_menu_nopriv = array(); |
| | 202 | |
| | 203 | // Loop over submenus and remove pages for which the user does not have privs. |
| | 204 | foreach ( $submenu as $parent => $sub ) { |
| | 205 | foreach ( $sub as $index => $data ) { |
| | 206 | if ( ! current_user_can( $data[1] ) ) { |
| | 207 | unset( $submenu[ $parent ][ $index ] ); |
| | 208 | $_wp_submenu_nopriv[ $parent ][ $data[2] ] = true; |
| | 209 | } |
| | 210 | } |
| | 211 | unset( $index, $data ); |
| | 212 | |
| | 213 | if ( empty( $submenu[ $parent ] ) ) { |
| | 214 | unset( $submenu[ $parent ] ); |
| | 215 | } |
| | 216 | } |
| | 217 | unset( $sub, $parent ); |
| | 218 | |
| | 219 | /* |
| | 220 | * Loop over the top-level menu. |
| | 221 | * Menus for which the original parent is not accessible due to lack of privileges |
| | 222 | * will have the next submenu in line be assigned as the new menu parent. |
| | 223 | */ |
| | 224 | foreach ( $menu as $id => $data ) { |
| | 225 | if ( empty( $submenu[ $data[2] ] ) ) { |
| | 226 | continue; |
| | 227 | } |
| | 228 | $subs = $submenu[ $data[2] ]; |
| | 229 | $first_sub = reset( $subs ); |
| | 230 | $old_parent = $data[2]; |
| | 231 | $new_parent = $first_sub[2]; |
| | 232 | /* |
| | 233 | * If the first submenu is not the same as the assigned parent, |
| | 234 | * make the first submenu the new parent. |
| | 235 | */ |
| | 236 | if ( $new_parent != $old_parent ) { |
| | 237 | $_wp_real_parent_file[ $old_parent ] = $new_parent; |
| | 238 | $menu[ $id ][2] = $new_parent; |
| | 239 | |
| | 240 | foreach ( $submenu[ $old_parent ] as $index => $data ) { |
| | 241 | $submenu[ $new_parent ][ $index ] = $submenu[ $old_parent ][ $index ]; |
| | 242 | unset( $submenu[ $old_parent ][ $index ] ); |
| | 243 | } |
| | 244 | unset( $submenu[ $old_parent ], $index ); |
| | 245 | |
| | 246 | if ( isset( $_wp_submenu_nopriv[ $old_parent ] ) ) { |
| | 247 | $_wp_submenu_nopriv[ $new_parent ] = $_wp_submenu_nopriv[ $old_parent ]; |
| | 248 | } |
| | 249 | } |
| | 250 | } |
| | 251 | unset( $id, $data, $subs, $first_sub, $old_parent, $new_parent ); |
| | 252 | |
| | 253 | /* |
| | 254 | * Remove menus that have no accessible submenus and require privileges |
| | 255 | * that the user does not have. Run re-parent loop again. |
| | 256 | */ |
| | 257 | foreach ( $menu as $id => $data ) { |
| | 258 | if ( ! current_user_can( $data[1] ) ) { |
| | 259 | $_wp_menu_nopriv[ $data[2] ] = true; |
| | 260 | } |
| | 261 | |
| | 262 | /* |
| | 263 | * If there is only one submenu and it is has same destination as the parent, |
| | 264 | * remove the submenu. |
| | 265 | */ |
| | 266 | if ( ! empty( $submenu[ $data[2] ] ) && 1 == count( $submenu[ $data[2] ] ) ) { |
| | 267 | $subs = $submenu[ $data[2] ]; |
| | 268 | $first_sub = reset( $subs ); |
| | 269 | if ( $data[2] == $first_sub[2] ) { |
| | 270 | unset( $submenu[ $data[2] ] ); |
| | 271 | } |
| | 272 | } |
| | 273 | |
| | 274 | // If submenu is empty... |
| | 275 | if ( empty( $submenu[ $data[2] ] ) ) { |
| | 276 | // And user doesn't have privs, remove menu. |
| | 277 | if ( isset( $_wp_menu_nopriv[ $data[2] ] ) ) { |
| | 278 | unset( $menu[ $id ] ); |
| | 279 | } |
| | 280 | } |
| | 281 | } |
| | 282 | unset( $id, $data, $subs, $first_sub ); |
| | 283 | } |
| | 284 | } |
| | 285 | No newline at end of file |