> */ public function get_abilities(): array { if ( ! function_exists( 'wp_get_abilities' ) ) { return array(); } $registered_names = get_option( 'ability_guard_registered_abilities', array() ); $registered_names = is_array( $registered_names ) ? array_values( array_unique( array_filter( array_map( 'sanitize_text_field', $registered_names ) ) ) ) : array(); $abilities = wp_get_abilities(); $items = array(); $seen_names = array(); foreach ( $abilities as $ability ) { if ( ! is_object( $ability ) || ! method_exists( $ability, 'get_name' ) ) { continue; } $meta = $this->call_getter( $ability, 'get_meta', array() ); $annotations = isset( $meta['annotations'] ) && is_array( $meta['annotations'] ) ? $meta['annotations'] : array(); $name = (string) $this->call_getter( $ability, 'get_name', '' ); $category = (string) $this->call_getter( $ability, 'get_category', '' ); $items[] = array( 'name' => $name, 'label' => (string) $this->call_getter( $ability, 'get_label', '' ), 'description' => (string) $this->call_getter( $ability, 'get_description', '' ), 'category' => $category, 'category_label' => $this->get_category_label( $category ), 'namespace' => $this->get_namespace( $name ), 'source' => $this->detect_source( $name ), 'readonly' => $this->normalize_annotation( $annotations, 'readonly' ), 'destructive' => $this->normalize_annotation( $annotations, 'destructive' ), 'idempotent' => $this->normalize_annotation( $annotations, 'idempotent' ), 'risk' => $this->determine_risk( $annotations ), 'show_in_rest' => ! empty( $meta['show_in_rest'] ), 'has_input_schema' => ! empty( $this->call_getter( $ability, 'get_input_schema', array() ) ), 'has_output_schema' => ! empty( $this->call_getter( $ability, 'get_output_schema', array() ) ), 'input_schema' => $this->call_getter( $ability, 'get_input_schema', array() ), 'output_schema' => $this->call_getter( $ability, 'get_output_schema', array() ), 'permission_status' => $this->get_permission_status( $ability ), 'meta' => $meta, ); $seen_names[] = $name; } foreach ( $registered_names as $registered_name ) { if ( in_array( $registered_name, $seen_names, true ) ) { continue; } $items[] = array( 'name' => $registered_name, 'label' => '', 'description' => '', 'category' => '', 'category_label' => '', 'namespace' => $this->get_namespace( $registered_name ), 'source' => $this->detect_source( $registered_name ), 'readonly' => null, 'destructive' => null, 'idempotent' => null, 'risk' => 'unknown', 'show_in_rest' => false, 'has_input_schema' => false, 'has_output_schema' => false, 'input_schema' => array(), 'output_schema' => array(), 'permission_status' => 'unknown', 'meta' => array(), ); } usort( $items, static function ( array $a, array $b ): int { return strcmp( $a['name'], $b['name'] ); } ); return $items; } /** * Get one normalized ability by name. * * @param string $name Ability name. * @return array|null */ public function get_ability( string $name ) { foreach ( $this->get_abilities() as $ability ) { if ( $ability['name'] === $name ) { return $ability; } } return null; } /** * Count abilities by risk. * * @param array> $abilities Abilities. * @return array */ public function get_risk_counts( array $abilities ): array { $counts = array( 'high' => 0, 'medium' => 0, 'low' => 0, 'unknown' => 0, ); foreach ( $abilities as $ability ) { $risk = isset( $ability['risk'] ) ? (string) $ability['risk'] : 'unknown'; if ( ! isset( $counts[ $risk ] ) ) { $risk = 'unknown'; } ++$counts[ $risk ]; } return $counts; } /** * Safely call an ability getter. * * @param object $ability_object Ability object. * @param string $method Getter method. * @param mixed $default_value Default value. * @return mixed */ private function call_getter( $ability_object, string $method, $default_value ) { if ( ! method_exists( $ability_object, $method ) ) { return $default_value; } $value = $ability_object->$method(); return null === $value ? $default_value : $value; } /** * Get category label. * * @param string $slug Category slug. * @return string */ private function get_category_label( string $slug ): string { if ( '' === $slug || ! function_exists( 'wp_get_ability_category' ) ) { return $slug; } $category = wp_get_ability_category( $slug ); if ( is_object( $category ) && method_exists( $category, 'get_label' ) ) { return (string) $category->get_label(); } return $slug; } /** * Get namespace from ability name. * * @param string $name Ability name. * @return string */ private function get_namespace( string $name ): string { $parts = explode( '/', $name, 2 ); return $parts[0] ?? $name; } /** * Detect a basic source label from the ability namespace. * * @param string $name Ability name. * @return string */ private function detect_source( string $name ): string { $namespace = $this->get_namespace( $name ); if ( ! function_exists( 'get_plugins' ) && defined( 'ABSPATH' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } if ( function_exists( 'get_plugins' ) ) { $plugins = get_plugins(); foreach ( $plugins as $plugin_file => $plugin_data ) { $slug = dirname( $plugin_file ); if ( '.' === $slug ) { $slug = basename( $plugin_file, '.php' ); } if ( $slug === $namespace ) { return isset( $plugin_data['Name'] ) ? (string) $plugin_data['Name'] : $slug; } } } if ( in_array( $namespace, array( 'core', 'wordpress', 'wp' ), true ) ) { return __( 'WordPress Core', 'ability-guard' ); } return sprintf( /* translators: %s: ability namespace. */ __( 'Namespace: %s', 'ability-guard' ), $namespace ); } /** * Normalize annotation to true, false, or null. * * @param array $annotations Ability annotations. * @param string $key Annotation key. * @return bool|null */ private function normalize_annotation( array $annotations, string $key ) { if ( ! array_key_exists( $key, $annotations ) ) { return null; } if ( true === $annotations[ $key ] ) { return true; } if ( false === $annotations[ $key ] ) { return false; } return null; } /** * Determine risk from annotations. * * @param array $annotations Ability annotations. * @return string */ private function determine_risk( array $annotations ): string { $readonly = $this->normalize_annotation( $annotations, 'readonly' ); $destructive = $this->normalize_annotation( $annotations, 'destructive' ); if ( true === $destructive ) { return 'high'; } if ( true === $readonly ) { return 'low'; } if ( false === $destructive ) { return 'medium'; } return 'unknown'; } /** * Check whether the current user can execute the ability with default input. * * @param object $ability Ability object. * @return string */ private function get_permission_status( $ability ): string { if ( ! method_exists( $ability, 'check_permissions' ) ) { return 'unknown'; } $input = null; if ( method_exists( $ability, 'normalize_input' ) ) { $input = $ability->normalize_input(); } try { $result = $ability->check_permissions( $input ); } catch ( Throwable $e ) { return 'unknown'; } if ( true === $result ) { return 'allowed'; } if ( false === $result || is_wp_error( $result ) ) { return 'denied'; } return 'unknown'; } }