| 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 |