Admin, roles and capability hooks

These five hooks cover the HappenBoard admin surface: the submenu manifest that feeds both the WordPress menu and the in-app sidebar rail, the bootstrap payload handed to the React app, and the filters around the plugin’s custom capability model.

The two role-default filters behave differently from everything else here. They run inside the capability installer, not on every request, so editing a mapping changes nothing until that installer runs again.

happenboard_admin_submenus

Filter Free Since 1.0 src/Admin/Menu.php

The single manifest behind both the WordPress submenu pages under the HappenBoard top-level menu and the in-app sidebar rail. Add, remove or reorder admin screens here.

Signature

apply_filters( 'happenboard_admin_submenus', array<int,array{path:string,label:string,capability:string,parent:string}> $default )

Parameters

$default array<int,array{path:string,label:string,capability:string,parent:string}>
The eleven built-in rows, from Dashboard to Settings. path is the SPA route (/ for the dashboard), label is already translated, capability is a custom HappenBoard capability, and parent is empty for a top-level rail item or the path of the row it nests under, for example /bookings for Booking types and Availability.

Return value

array<int,array{path:string,label:string,capability:string,parent:string}> The submenu list, in display order. Rows are registered with add_submenu_page() in the order they appear.

Example

add_filter(
    'happenboard_admin_submenus',
    function ( $submenus ) {
        // Append. Returning a fresh array would drop the built-in screens
        // and the two rows HappenBoard Pro adds at priority 10.
        $submenus[] = array(
            'path'       => '/reports',
            'label'      => __( 'Reports', 'my-plugin' ),
            'capability' => 'happenboard_view_attendees',
            'parent'     => '',
        );

        return $submenus;
    },
    20
);

Notes

  • Append, do not replace. HappenBoard Pro registers its own callback at priority 10 and adds /orders (spliced in after /organizers) and /resources (nested under /bookings) to the array it receives. A later callback that returns a fresh array removes both Pro screens along with the built-in ones.
  • It fires more than once per admin request: from Menu::register_menu() on admin_menu, and again from ScreenContext::build_nav() through the public Menu::submenus() when the SPA bootstrap payload is built. Keep the callback pure, and register it before admin_menu runs.
  • Every row needs path, label and capability: register_menu() reads those three with no default, so a missing key is a PHP warning and a broken entry. parent and external are optional. Setting external to true makes path a real admin file such as edit-tags.php?taxonomy=... and attaches no render callback.
  • Labels are output as given. register_menu() passes the value straight to add_submenu_page() with no translation call, so wrap your own label in __(). Pro’s two rows are currently plain untranslated literals.
  • The capability is enforced twice: by add_submenu_page(), and again by ScreenContext::build_nav(), which drops rows the current user cannot access from happenBoardVars.nav. A row with an empty capability disappears from the rail entirely.

happenboard_capabilities_role_defaults

Filter Free Since 1.0 src/Capabilities/Capabilities.php

Changes which WordPress roles receive which HappenBoard capabilities when the capability installer runs. It is applied inside Capabilities::install(), which fires on plugin activation and on the versioned backfill, not on every request.

Signature

apply_filters( 'happenboard_capabilities_role_defaults', array<string,array<int,string>> $mapping )

Parameters

$mapping array<string,array<int,string>>
The defaults: administrator gets all ten free capabilities, editor gets seven of them (everything except happenboard_export_attendees and happenboard_manage_integrations). happenboard_host is granted to no role by default and is meant to be assigned per user.

Return value

array<string,array<int,string>> A map of role slug to a list of capability slugs. Each entry is written with WP_Role::add_cap().

Example

add_filter(
    // Put this in an mu-plugin so it is registered before init:1, then
    // deactivate and reactivate HappenBoard once to apply it.
    'happenboard_capabilities_role_defaults',
    function ( $mapping ) {
        $mapping['shop_manager'] = array(
            'happenboard_view_bookings',
            'happenboard_manage_availability',
        );

        // Stop granting bulk attendee export to administrators.
        $mapping['administrator'] = array_values(
            array_diff( $mapping['administrator'], array( 'happenboard_export_attendees' ) )
        );

        return $mapping;
    }
);

Notes

  • It does not run on every request, so changing the mapping grants nothing retroactively. Capabilities::install() is reached from Plugin::activate() (the activation hook) and from Plugin::maybe_install_capabilities(), hooked on init at priority 1, which returns early unless the happenboard_caps_version option is below the code’s CAPS_VERSION (currently 3). On an up-to-date install neither path runs again. To apply a change, deactivate and reactivate HappenBoard, or delete the happenboard_caps_version option (and the legacy happenboard_caps_installed flag).
  • Register the callback before init priority 1: an mu-plugin, a plugin file body, or plugins_loaded. A callback added on init at the default priority 10 is too late for the backfill path.
  • Additive only. install() calls WP_Role::add_cap() and never remove_cap(), so taking a capability out of the mapping does not revoke it from roles that already have it: call WP_Role::remove_cap() yourself. Role slugs that do not exist are skipped silently.
  • Uninstall does not see your changes. Capabilities::uninstall() builds its removal list from the hard-coded ROLE_DEFAULTS constant rather than the filtered mapping, so capabilities you added through this filter survive removal of the plugin.
  • The class docblock says Pro extends this list through the same filter. It does not: HappenBoard Pro ships its own happenboard_pro_capabilities_role_defaults filter and its own installer, and registers no callback here.

happenboard_me_capabilities

Filter Free Since 1.0 src/REST/Me.php

Adds to or overrides the capability map returned by GET /happenboard/v1/me/capabilities. The admin app and headless front ends read that response to decide which controls to render.

Signature

apply_filters( 'happenboard_me_capabilities', array<string,bool> $caps, \WP_User $user )

Parameters

$caps array<string,bool>
Seven free capability slugs mapped to their current_user_can() result: manage events, manage venues, manage organizers, view attendees, export attendees, manage settings, manage integrations. The three booking capabilities and happenboard_host are not in the base map even though the free plugin defines them.
$user \WP_User
The result of wp_get_current_user() for this request. Always a logged-in user: the route’s permission callback is is_user_logged_in().

Return value

array<string,bool> A map of capability slug to boolean, serialized as-is into the capabilities key of the response.

Example

add_filter(
    'happenboard_me_capabilities',
    function ( $caps, $user ) {
        // The booking capabilities are missing from the base matrix.
        $caps['happenboard_view_bookings']        = user_can( $user, 'happenboard_view_bookings' );
        $caps['happenboard_manage_booking_types'] = user_can( $user, 'happenboard_manage_booking_types' );
        $caps['happenboard_manage_availability']  = user_can( $user, 'happenboard_manage_availability' );
        $caps['happenboard_host']                 = user_can( $user, 'happenboard_host' );

        return $caps;
    },
    10,
    2
);

Notes

  • Two arguments: register with add_filter( ..., 10, 2 ).
  • This is a reporting surface, not an authorization one. The endpoint reflects what current_user_can() says; flipping a value to true only makes the interface show a control whose own REST permission callback will still answer 403.
  • HappenBoard Pro already hooks it at priority 10 with 2 arguments and adds its six capabilities (manage_tickets, view_orders, export_orders, refund_orders, run_checkin, manage_resources). Append to the array you receive: returning a fresh one wipes both the free and the Pro entries.
  • The filtered array is used with no cast and no type check. Non-boolean values go straight into the JSON, so a client doing a strict comparison against true and one doing a truthy check will disagree. Keep the values boolean.
  • Any logged-in user, down to a Subscriber, can call this route. Do not key the map on anything you would not expose to every account on the site; the handler deliberately omits the user’s email address for the same reason.

happenboard_pro_capabilities_role_defaults

Filter Pro Since 1.0 src/Capabilities/Capabilities.php

The Pro counterpart of happenboard_capabilities_role_defaults, covering the six ticketing and resource capabilities. It is applied inside HappenBoard\Pro\Capabilities\Capabilities::install().

Signature

apply_filters( 'happenboard_pro_capabilities_role_defaults', array<string,array<int,string>> $mapping )

Parameters

$mapping array<string,array<int,string>>
The defaults: administrator gets all six (happenboard_manage_tickets, happenboard_view_orders, happenboard_export_orders, happenboard_refund_orders, happenboard_run_checkin, happenboard_manage_resources), editor gets four (no export, no refund).

Return value

array<string,array<int,string>> A map of role slug to a list of capability slugs, written with WP_Role::add_cap().

Example

add_filter(
    // mu-plugin, registered before init:1. Takes effect on the next Pro
    // activation or CAPS_VERSION bump.
    'happenboard_pro_capabilities_role_defaults',
    function ( $mapping ) {
        $mapping['accountant'] = array(
            'happenboard_view_orders',
            'happenboard_export_orders',
            'happenboard_refund_orders',
        );

        return $mapping;
    }
);

Notes

  • Same timing rule as the free filter. install() is reached from Pro’s activation hook (Pro\Plugin::activate()) and from Pro\Plugin::maybe_install_capabilities() on init at priority 1, which returns early unless the happenboard_pro_caps_version option is below Pro’s CAPS_VERSION (currently 3). Editing the mapping on a running site changes nothing until one of those paths runs.
  • Register the callback before init priority 1, in an mu-plugin or on plugins_loaded.
  • Additive only: install() calls WP_Role::add_cap() and never removes anything, and role slugs that do not exist are skipped without warning.
  • happenboard_manage_integrations is deliberately absent from Pro’s mapping because it belongs to the free plugin’s capability class, which Pro reuses in its REST permission callbacks. Grant it through happenboard_capabilities_role_defaults instead.
  • Pro\Capabilities\Capabilities::uninstall() removes only the capabilities listed in the hard-coded ROLE_DEFAULTS constant, so anything you add through this filter stays on the roles after Pro is uninstalled.

happenboard_screen_context

Filter Free Since 1.0 src/Admin/ScreenContext.php

The bootstrap payload handed to the admin React app as window.happenBoardVars. Add your own configuration keys here, or repoint the built-in ones such as the sidebar footer links.

Signature

apply_filters( 'happenboard_screen_context', array<string,mixed> $context )

Parameters

$context array<string,mixed>
The assembled payload: pluginVersion, nonce (a wp_rest nonce), restBase, wpRestBase, siteUrl, siteName, adminEmail, adminUrl, pluginUrl, nav, links, currentUser, locale, timezone, dateFormat, timeFormat, startOfWeek, integrations, onboarded, general and colorPresets.

Return value

array<string,mixed> The context array, passed straight to wp_localize_script().

Example

add_filter(
    'happenboard_screen_context',
    function ( $context ) {
        // Repoint the sidebar footer links at an internal helpdesk.
        $context['links']['support'] = 'https://example.com/helpdesk';

        // Own namespace, nested so the booleans stay booleans in JS.
        $context['myPlugin'] = array(
            'enabled'  => true,
            'endpoint' => rest_url( 'my-plugin/v1/' ),
        );

        return $context;
    },
    20
);

Notes

  • It only runs on HappenBoard admin screens. ScreenContext::build() is called from Admin\Assets::enqueue(), which returns early unless the current screen ID contains happenboard. The block editor receives a different, unfiltered blob that also happens to be named happenBoardVars, built in Admin\PostEditor, so this filter never reaches it.
  • Merge, do not replace. HappenBoard Pro hooks it at priority 10 to set pro.active, pro.licenseStatus and pro.version; a callback returning a fresh array removes them and the admin app stops rendering its Pro routes.
  • The result goes through wp_localize_script(), which JSON-encodes it, so no escaping is needed on your side. It also casts top-level scalars to strings, so a boolean added at the top level reaches JavaScript as "1" or "". Nest flags inside an array, the way integrations does, to keep real booleans.
  • The payload is printed inline on every HappenBoard admin page for every user who can reach one, and context['general'] is already the full General settings array. Do not add API secrets, license keys or webhook signing secrets.
  • There is no pro key in the base context, despite Pro’s own docblock saying the free plugin declares one. Pro creates it defensively with $context['pro'] ?? null; do the same if you read it. Note also that nav is built from happenboard_admin_submenus, so building this context fires that filter too.