$input Ability input. * @return true|WP_Error */ public static function assert_confirmed( array $input ) { if ( empty( $input['confirm'] ) ) { return new WP_Error( 'confirmation_required', __( 'This operation is irreversible. Pass confirm: true to proceed.', 'acrossai-abilities-manager' ) ); } return true; } /** * Check a Rank Math granular capability. * * Deliberately re-implements \RankMath\Helper::has_cap() rather than calling * it (same semantics as includes/helpers/class-wordpress.php:54) for two * reasons: it cannot fatal when Rank Math is absent, and it keeps * permission_callback free of cross-plugin coupling. * * The '-' to '_' normalisation matters — the module id is '404-monitor' but * the capability is 'rank_math_404_monitor'. * * @param string $cap Capability suffix, e.g. 'titles', 'redirections', '404-monitor'. * @return bool */ public static function has_cap( string $cap ): bool { if ( '' === $cap ) { return true; } return current_user_can( 'rank_math_' . str_replace( '-', '_', $cap ) ); } /** * Build the permission_callback for an ability. * * Composes the house capability floor with Rank Math's own granular * capability. AND is never looser than either model: it is tighter than a * blanket manage_options (which would ignore the Role Manager grants a site * owner deliberately configured) and tighter than Rank Math's model alone. * * Pass an empty $rm_cap for abilities Rank Math itself gates on * manage_options, where there is no granular capability to compose. * * @param string $rm_cap Rank Math capability suffix, or '' for floor only. * @param string $floor WordPress capability floor. * @return callable(): bool */ public static function can( string $rm_cap, string $floor = 'manage_options' ): callable { return static function () use ( $rm_cap, $floor ): bool { $floor_ok = current_user_can( $floor ); $cap_ok = self::has_cap( $rm_cap ); $allowed = $floor_ok && $cap_ok; /** * Filter whether the current user may execute a Rank Math ability. * * Unlike the other suites' permission filters, this one may WIDEN as well as narrow: * returning true admits Rank Math's own looser model — an editor holding * rank_math_titles but not manage_options. That is the documented purpose and is * preserved. * * What it may not do is admit a user holding neither. The result is bounded below by * "clears the floor, or holds the granular Rank Math capability this ability names", so * a filter can substitute one model for the other but cannot grant access to a * subscriber. The bound is deliberately keyed on `'' !== $rm_cap`: has_cap() returns * true when no granular capability applies, so without that check the bound would be * vacuous for exactly the abilities that have nothing but the floor protecting them. * * @param bool $allowed Result of floor AND granular capability. * @param string $rm_cap Rank Math capability suffix, '' when none applies. * @param string $floor WordPress capability floor. */ $filtered = (bool) apply_filters( self::PERMISSION_FILTER, $allowed, $rm_cap, $floor ); return $filtered && ( $floor_ok || ( '' !== $rm_cap && $cap_ok ) ); }; } /** * Build a success envelope. * * @param array $payload Ability-specific keys. * @param string $message Human-readable summary. * @return array */ public static function ok( array $payload, string $message ): array { unset( $payload['success'], $payload['message'], $payload['error_code'] ); return array_merge( array( 'success' => true ), $payload, array( 'message' => $message ) ); } /** * Build a failure envelope from a WP_Error. * * A raw WP_Error is never returned from execute() — this is where it is * unwrapped. * * @param WP_Error $error Error to unwrap. * @param array $context Optional identifying input to echo back. * @return array */ public static function fail( WP_Error $error, array $context = array() ): array { return self::error( (string) $error->get_error_code(), (string) $error->get_error_message(), $context ); } /** * Build a failure envelope from an explicit code and message. * * @param string $code Machine-readable error code. * @param string $message Human-readable message. * @param array $context Optional identifying input to echo back. * @return array */ public static function error( string $code, string $message, array $context = array() ): array { // Context often echoes caller-supplied input back. Strip the reserved // envelope keys so a caller can never spoof success or the error code. unset( $context['success'], $context['message'], $context['error_code'] ); return array_merge( array( 'success' => false ), $context, array( 'message' => $message, 'error_code' => $code, ) ); } }