How to check permissions with current_user_can in PHP in WordPress

Contents

Introduction

This article is an exhaustive, practical reference on how to check permissions in WordPress using current_user_can() in PHP. It covers how the function works, when and where to call it, the difference between roles and capabilities, meta-capabilities (such as edit_post), multisite and REST/AJAX considerations, security and performance best practices, common pitfalls, and many copy-paste-ready examples.

What current_user_can() does

current_user_can() checks whether the currently logged-in user has a given capability. Internally it calls user_can() on the global current user object. If the required capability is a meta-capability (for example, edit_post), WordPress maps it to one or more primitive capabilities using map_meta_cap before making the decision.

Key points

  • Input: a capability name string, optionally followed by additional arguments (e.g., a post ID).
  • Return: boolean (true if current user has capability).
  • Uses: control access to UI and server-side actions. Always check capabilities server-side when performing sensitive operations.
  • Related function: user_can( user, cap ) — you can test any user object or ID, not only the current user.

When you can call it (timing)

Call current_user_can() only after WordPress has set the current user. In practice, safe places include hooks that run during or after authentication and initialization, such as init, admin_init, template_redirect, wp_loaded, AJAX actions (after authentication happens), REST permission callbacks, and later. If you call it too early (for example during plugin file load before the pluggable functions and current user setup), you may get unreliable results or false.

Example timing usage

  1. Adding admin pages conditionally: use admin_menu or admin_init.
  2. Processing form submissions: check capability in the form handler hook that runs after init.
  3. AJAX: check capability inside the registered action handler.
  4. REST API: perform capability checks inside the permission_callback for the route.

Roles vs Capabilities

A role is a collection of capabilities. A capability is a granular permission (like edit_posts). Functions like current_user_can(edit_posts) check whether the current users role(s) grant the capability (or whether the user was given the capability directly).

Common core capabilities

Content edit_posts, publish_posts, edit_others_posts, delete_posts, delete_others_posts
Pages edit_pages, publish_pages, delete_pages, edit_others_pages
Admin manage_options, edit_theme_options, activate_plugins, install_plugins
Users list_users, promote_users, edit_users
Network (Multisite) manage_network, manage_sites and is_super_admin() for true super-admin

Meta-capabilities vs Primitive capabilities

Meta-capabilities are abstract capabilities that require extra context to translate into primitive capabilities. Examples: edit_post, delete_post, read_post. They need extra parameters (like a post ID) because whether a user can edit a post depends on ownership, post type, and other conditions. WordPress translates meta-capabilities to primitive capabilities with map_meta_cap.

Example: edit_post

// Typical check before showing an Edit link or before processing edit actions.
if ( current_user_can( edit_post, post_id ) ) {
    // show link or allow edit
} else {
    // deny access
}

Basic usage examples

Simple capability check

if ( current_user_can( manage_options ) ) {
    // This code runs only for users who can manage WordPress options (usually admins).
}

Check and abort on insufficient permissions (server-side enforcement)

if ( ! current_user_can( edit_posts ) ) {
    wp_die( You do not have permission to edit posts. )
}
// safe to process edit logic

Use user_can() to check a different user

// user_id may be any integer, or use WP_User instance
user = get_user_by( id, user_id )
if ( user  user_can( user, edit_pages ) ) {
    // user can edit pages
}

Practical examples and patterns

1) Conditionally add admin menu item

add_action( admin_menu, function() {
    if ( current_user_can( manage_options ) ) {
        add_menu_page( My Plugin, My Plugin, manage_options, my-plugin, my_plugin_page_callback )
    }
} )

2) Protecting form submissions (POST processing)

add_action( admin_post_save_my_item, function() {
    // Verify nonce first
    if ( ! isset( _POST[_wpnonce] )  ! wp_verify_nonce( _POST[_wpnonce], save_my_item ) ) {
        wp_die( Invalid nonce. )
    }

    // Capability check
    if ( ! current_user_can( edit_posts ) ) {
        wp_die( Insufficient permissions. )
    }

    // Proceed with sanitized processing
} )

3) Protecting AJAX handlers

add_action( wp_ajax_my_action, function() {
    check_ajax_referer( my_action_nonce, _wpnonce, true )

    if ( ! current_user_can( edit_post, absint( _POST[post_id] ) ) ) {
        wp_send_json_error( Forbidden, 403 )
    }

    // perform action and return success
    wp_send_json_success()
} )

4) REST API permission_callback

register_rest_route(
    my-plugin/v1,
    /item/(?Pd ),
    array(
        methods  => POST,
        callback => my_plugin_update_item,
        permission_callback => function( request ) {
            id = (int) request[id]
            return current_user_can( edit_post, id )
        },
    )
)

5) Adding custom capabilities and checking them

// On plugin activation: add capability to a role
register_activation_hook( __FILE__, function() {
    role = get_role( editor )
    if ( role ) {
        role->add_cap( manage_special_feature )
    }
} )

// Later, check
if ( current_user_can( manage_special_feature ) ) {
    // allowed
}

6) Checking edit/delete on specific post (meta-capability)

post_id = 123
if ( current_user_can( edit_post, post_id ) ) {
    // User can edit that specific post
}

7) Avoid repeated checks in loops (cache result)

// If capability is the same inside a loop:
can_edit_posts = current_user_can( edit_posts )
foreach ( items as item ) {
    if ( can_edit_posts ) {
        // do something that requires edit_posts
    }
}

Advanced topics

map_meta_cap filter: custom logic for meta-capabilities

Use the map_meta_cap filter to alter how meta-capabilities are resolved into primitive capabilities. This is powerful for custom post types with complex ownership or for plugins that need nuanced permissions.

add_filter( map_meta_cap, function( caps, cap, user_id, args ) {
    if ( edit_special_item === cap ) {
        item_id = isset( args[0] ) ? (int) args[0] : 0
        // custom logic to decide capabilities required
        if ( some_custom_condition( user_id, item_id ) ) {
            caps = array( edit_posts )
        } else {
            caps = array( do_not_allow )
        }
    }
    return caps
}, 10, 4 )

Custom post types and capabilities

When registering custom post types, you can control capabilities with capability_type and capabilities args. If you map meta-capabilities correctly, current_user_can(edit_post, id) will respect your custom mapping.

Multisite (Network) caveats

  • is_super_admin() checks network super-admin. Some network-level tasks require super-admin, not just a site-level capability.
  • Network management capabilities may be different or require different checks (for example, activating plugins network-wide).

WP-CLI and cron jobs

In CLI or cron contexts there may be no current user — current_user_can() returns false. For CLI testing you can set the current user with wp_set_current_user() for internal cron tasks, either run actions with a known user set or use programmatic checks appropriate for the job.

Security best practices

  • Always check capabilities server-side before performing privilege-sensitive actions — do not rely solely on hiding UI.
  • Use nonces for form/JS requests and combine nonce verification with a capability check.
  • Sanitize and validate all input data before using it in capability-related checks (e.g., ensure post IDs are integers).
  • Prefer specific capabilities (e.g., edit_post) over broad ones whenever possible.
  • Do not assume a capability implies another — check exactly what you need.

Performance considerations

  • current_user_can() is reasonably fast, but avoid calling it repeatedly inside tight loops when the capability does not change cache the boolean in a variable.
  • Use user_can() if you already have a WP_User object to avoid extra lookups.

Common pitfalls and how to avoid them

  1. Calling too early: Make sure WP authentication has run. Use hooks like init or later.
  2. Relying on UI-only checks: Hidden buttons are not protection always check on the server.
  3. Not passing required args for meta-capabilities: For meta-capabilities like edit_post, supply the post ID.
  4. Assuming capabilities are identical across sites: Roles and capabilities can be altered by plugins or administrators — never hard-code assumptions about role names.
  5. Multisite confusion: Some tasks require super-admin use is_super_admin() and the appropriate network capabilities.

Examples: Complete patterns

Plugin activation: add capabilities to role

register_activation_hook( __FILE__, function() {
    // Add capability to role safely
    role = get_role( administrator )
    if ( role  ! role->has_cap( manage_special_feature ) ) {
        role->add_cap( manage_special_feature )
    }
} )

register_deactivation_hook( __FILE__, function() {
    // Optionally remove capability
    role = get_role( administrator )
    if ( role  role->has_cap( manage_special_feature ) ) {
        role->remove_cap( manage_special_feature )
    }
} )

Secure admin action with capability check and nonce

add_action( admin_post_my_plugin_action, function() {
    if ( ! isset( _POST[_wpnonce] )  ! wp_verify_nonce( _POST[_wpnonce], my_plugin_action ) ) {
        wp_die( Security check failed. )
    }

    if ( ! current_user_can( manage_options ) ) {
        wp_die( Insufficient permissions. )
    }

    // process safe data
} )

Customize map_meta_cap for custom logic

add_filter( map_meta_cap, function( caps, cap, user_id, args ) {
    // For a plugin-specific meta-capability that requires special business rules
    if ( edit_special === cap  ! empty( args[0] ) ) {
        special_id = (int) args[0]
        if ( is_user_allowed_to_edit_special( user_id, special_id ) ) {
            caps = array( edit_posts ) // require this primitive capability
        } else {
            caps = array( do_not_allow )
        }
    }
    return caps
}, 10, 4 )

Useful helper functions and alternatives

  • user_can( user, cap ): check capabilities for a particular user ID/object.
  • current_user_can_for_blog( blog_id, cap ) (deprecated in favor of switching blog or using user_can with the proper context) — for multisite you may need to switch to the site context temporarily.
  • is_user_logged_in(): check easily whether any user is logged in.
  • get_role() and add_cap()/remove_cap(): modify roles and capabilities programmatically.
  • map_meta_cap filter: intercept meta-capability mapping.

Checklist before deploying capability checks

  • Are you checking capability on the server side where the action is performed?
  • Have you verified the correct capability name (meta vs primitive)?
  • Are you passing the necessary arguments (e.g., post ID) for meta-capabilities?
  • Did you verify nonce or other anti-CSRF protections where appropriate?
  • Did you test behavior for non-logged-in users, admins, editors, and custom roles?
  • Did you avoid unnecessary repeated checks inside loops?

References

Summary

Use current_user_can() to gate functionality according to capabilities. Place checks where WordPress has initialized the current user (init or later). For meta-capabilities provide required context (IDs). Always enforce permissions server-side and combine checks with nonce verification for mutating actions. Use user_can() if you need to check another user. For advanced scenarios, use map_meta_cap to customize how meta-capabilities are resolved. Follow performance and security best practices: cache repeated checks, sanitize inputs, and test across roles and multisite contexts.



Acepto donaciones de BAT's mediante el navegador Brave 🙂



Leave a Reply

Your email address will not be published. Required fields are marked *