Skip to content

Capabilities & Permissions

Eventonomy ships one optional WordPress role, evnm_organizer (“Event Organizer”), which is registered but granted to nobody until you or another plugin grants it (see Granting organizer access below). Everything else is capability-based: Eventonomy defines seven of its own capabilities and maps each onto a capability your existing roles already have, so whoever can already do the equivalent thing in WordPress can do it here. Nothing to configure, and no role bloat left behind on uninstall.

Filterable via evnm_capabilities (Core\Capabilities::map()).

Eventonomy capability Maps to Effectively
evnm_create_events read Any logged-in user, including subscribers
evnm_edit_events read Any logged-in user - ownership-scoped, see below
evnm_edit_others_events edit_others_posts Editor and above
evnm_delete_events delete_posts Author and above
evnm_manage_rsvps manage_options Administrator - or the event’s owner
evnm_manage_orders manage_options Administrator - or the event’s owner
evnm_manage_settings manage_options Administrator

These caps are virtual - they are never stored on a role

Section titled “These caps are virtual - they are never stored on a role”

The seven meta-caps in the table above are never stored on a role, and neither plugin registers a map_meta_cap filter. (The one role Eventonomy does register, evnm_organizer, is separate from this table - see below.) Capabilities::user_can( $user_id, $meta_cap ) looks the meta-cap up in the table above and asks WordPress core about the primitive, so the effective grant is entirely core’s default role grid. That is what “no role bloat left behind on uninstall” means literally: there is nothing to clean up.

Two consequences worth knowing:

  • current_user_can( 'evnm_manage_settings' ) returns false for everyone, because no role holds that string. Always go through Capabilities::user_can() or one of the evnm_user_can_*() helpers, never a raw current_user_can() on an evnm_ cap. Admin screens do the same translation through AdminMenu::primitive_cap(), which falls back to manage_options for a meta-cap that is not in the map.
  • evnm_delete_events is defined but nothing reads it today. Deleting an event goes through evnm_user_can_manage_event( …, 'delete' ), which resolves via evnm_edit_events on the ownership branch and evnm_edit_others_events otherwise. Remapping evnm_delete_events will therefore not change who can delete an event; use the evnm_user_can_manage_event filter for that.

A community events plugin whose default is “only Editors may add an event” is useless out of the box. read is the every-registered-user primitive, so a subscriber can create an event and edit their own.

Two things keep that safe: editing is ownership-scoped (below), and publishing is governed by the approval queue rather than by the capability. Site owners narrow who may create at all with the creator_role setting, which evnm_user_can_create_events() enforces; the default is open.

Managing the attendees of an event you do not own is a site-manager action, not a content-editor one: it can burn check-in tokens, cancel people’s registrations, and bulk-email them. Orders are money- and identity-adjacent for the same reason.

This does not restrict organizers. The event’s own author passes through the ownership branch described next, so a member-organizer with no WordPress editing capabilities still manages their own attendees and orders in full. What is removed is only cross-event reach - an Editor cannot read every order on the site.

Never test a raw capability for an event-scoped action. Ask the helper, which resolves ownership first and the capability second:

if ( ! evnm_user_can_manage_event( get_current_user_id(), $event, 'manage_rsvps' ) ) {
return new WP_Error( 'evnm_forbidden', __( 'Not allowed.' ), array( 'status' => 403 ) );
}

evnm_user_can_manage_event( int $user_id, array $event, string $action ): bool

  • The event’s author passes for their own event.
  • A non-owner must hold the mapped capability (manage_options for manage_rsvps / manage_orders, edit_others_posts for editing).

evnm_user_can_manage_order( int $user_id, array $order ): bool is the order-scoped twin: an administrator passes for any order, and the owner of the order’s event passes for that order. The buyer does not - which is why a buyer cannot refund their own purchase.

evnm_user_can_create_events( int $user_id ): bool folds in the creator_role setting. Use it rather than testing evnm_create_events directly, or you will ignore the site owner’s restriction.

Two rules the whole REST surface follows, and yours should too:

  1. Return a WP_Error with a status, never false. false produces a generic 403 with no code a client can branch on. Return 401 for “not logged in” and 403 for “logged in, not allowed” so an app can tell “sign in” from “you cannot do this”.
  2. Every route has a real permission_callback. __return_true is allowed only where public access is the point, and each such route is allowlisted with a documented reason (the discovery feed, the gateway webhooks, the Connect OAuth return). A route with no callback at all fails CI.
'permission_callback' => function () {
if ( ! is_user_logged_in() ) {
return new WP_Error( 'evnm_unauthorized', __( 'Sign in first.' ), array( 'status' => 401 ) );
}
if ( ! current_user_can( 'evnm_manage_settings' ) ) {
return new WP_Error( 'evnm_forbidden', __( 'Not allowed.' ), array( 'status' => 403 ) );
}
return true;
},

Some fields are withheld from output regardless of who is asking:

  • magic_token, checkin_token - never in any REST response or admin table.
  • gateway_txn_id - restricted to manage_options.
  • The order meta blob - stripped entirely from /orders responses; it holds the access token, guest emails and the refund ledger.

If you extend a response, do not re-introduce these. The rule is that a capability check decides whether you get the row, and a separate rule decides which fields come with it.

add_filter( 'evnm_capabilities', function ( array $map ) {
// Let Editors manage attendees on any event (loosens the default).
$map['evnm_manage_rsvps'] = 'edit_others_posts';
return $map;
} );

Loosening manage_rsvps or manage_orders grants cross-event reach over attendee identity and money. Do it deliberately, and read the reasoning above first.

evnm_organizer is a registered WordPress role that also carries a marker capability of the same name (includes/Core/Roles.php). The same-name capability is deliberate: both add_role( 'evnm_organizer' ) and add_cap( 'evnm_organizer' ) make user_can( $id, 'evnm_organizer' ) true, so whichever a consumer reaches for works.

Resolution goes through core user_can(). There is no plugin-internal organizer state - no setting, no table lookup, no user meta - so a role or capability granted by another plugin is honoured immediately.

Use the first-party API (includes/capability-api.php):

evnm_grant_organizer_role( $user_id ); // grant
evnm_revoke_organizer_role( $user_id ); // revoke
evnm_user_has_organizer_role( $user_id ); // read

Guard the call so your integration fails closed if Eventonomy is not active:

add_action( 'my_plugin_expert_approved', function ( $user_id ) {
if ( function_exists( 'evnm_grant_organizer_role' ) ) {
evnm_grant_organizer_role( (int) $user_id );
}
} );

The role is additive - the member keeps whatever role they already had.

No Eventonomy record needs to exist first. An organizer is created lazily from author_id on the first event save, so there is no half-state to provision. Creating an event and selling tickets both work immediately after the grant; a Stripe Connect account is optional and only affects how payouts are split, not whether a sale completes.

Do not gate on Capabilities::user_can( $id, 'evnm_organizer' ) - that method is a meta-cap translator for the seven caps in the table above, and evnm_organizer is not one of them, so it returns false for everyone. Use evnm_user_has_organizer_role().

To elevate without touching roles at all, filter evnm_user_can_create_events (( bool $can, int $user_id )); it is elevate-only and never revokes.

On uninstall the role is removed with remove_role().