logger = $logger; // Run as late as possible on the same hook everyone else uses to // register abilities, so every plugin (including ones that hook in // late themselves) has already had a chance to register before we // remove the ones the admin turned off. add_action('wp_abilities_api_init', array($this, 'apply_disabled_abilities'), PHP_INT_MAX); add_filter('wp_register_ability_args', array($this, 'public_core_abilities'), 10, 2); add_action('admin_post_aizen_save_abilities', array($this, 'handle_save')); add_action('admin_post_aizen_bulk_abilities', array($this, 'handle_bulk')); } /** * Every ability currently registered with WordPress core, regardless of * which plugin registered it. This is always read live via * wp_get_abilities() - AbiliZen keeps no cache/duplicate of its own. * * @return array */ public function get_all(): array { if (! function_exists('wp_get_abilities')) { return array(); } return wp_get_abilities(); } /** * Same list, grouped by category slug, sorted by label within each * category. Categories come from wp_get_ability_categories() when * available, otherwise abilities are grouped under "uncategorized". * * @return array */ public function get_grouped(): array { $abilities = $this->get_all(); $groups = array(); $categories = function_exists('wp_get_ability_categories') ? wp_get_ability_categories() : array(); foreach ($abilities as $ability) { $cat_slug = method_exists($ability, 'get_category') ? (string) $ability->get_category() : ''; $cat_slug = $cat_slug ? $cat_slug : 'uncategorized'; if (! isset($groups[$cat_slug])) { $label = $cat_slug; if (isset($categories[$cat_slug]) && method_exists($categories[$cat_slug], 'get_label')) { $label = $categories[$cat_slug]->get_label(); } elseif ('uncategorized' === $cat_slug) { $label = __('Uncategorized', 'abilizen-ai-abilities'); } $groups[$cat_slug] = array( 'label' => $label, 'abilities' => array(), ); } $groups[$cat_slug]['abilities'][] = $ability; } ksort($groups); foreach ($groups as &$group) { usort( $group['abilities'], function ($a, $b) { return strcasecmp($this->get_label($a), $this->get_label($b)); } ); } return $groups; } /** * Ability names the site owner has switched off in the AbiliZen dashboard. * * @return string[] */ public function get_disabled(): array { $disabled = get_option(AIZEN_OPTION_DISABLED_ABILITIES, array()); return is_array($disabled) ? array_values(array_unique(array_map('sanitize_text_field', $disabled))) : array(); } /** * @param string $name Ability name, e.g. "core/get-user-info". */ public function is_enabled(string $name): bool { return ! in_array($name, $this->get_disabled(), true); } /** * Whether an ability is flagged public / mcp-public in its own meta, i.e. * whether the ability's own plugin opted it into MCP at registration * time. AbiliZen can only turn a public ability OFF - it cannot make a * genuinely private ability reachable over MCP, by design. * * @param WP_Ability $ability Ability instance. */ public function is_publicly_exposed($ability): bool { $meta = method_exists($ability, 'get_meta') ? (array) $ability->get_meta() : array(); if (isset($meta['mcp']['public'])) { return (bool) $meta['mcp']['public']; } if (isset($meta['public'])) { return (bool) $meta['public']; } return false; } /** * Best-effort human label for an ability, falling back to its name. * * @param WP_Ability $ability Ability instance. */ public function get_label($ability): string { if (method_exists($ability, 'get_label')) { $label = $ability->get_label(); if ($label) { return $label; } } return method_exists($ability, 'get_name') ? $ability->get_name() : ''; } /** * Removes every ability the admin disabled from the live registry, on * every request, right after all plugins finish registering theirs. * Because this runs on wp_abilities_api_init itself, disabled abilities * never make it into wp_get_abilities() in the first place - they are * invisible to MCP, to REST, and to any other code that queries the * registry, exactly like the ability was never registered at all. */ public function apply_disabled_abilities() { if (! function_exists('wp_unregister_ability')) { return; } $disabled = apply_filters('aizen_enforce_disabled_abilities', $this->get_disabled()); if (empty($disabled)) { return; } foreach ($disabled as $name) { if (function_exists('wp_has_ability')) { if (wp_has_ability($name)) { wp_unregister_ability($name); } } else { // Older/looser API surface: attempt unconditionally, quietly. wp_unregister_ability($name); } } } /** * Handles the "Save changes" form from the Abilities tab. */ public function handle_save() { if (! current_user_can('manage_options')) { wp_die(esc_html__('You do not have permission to do this.', 'abilizen-ai-abilities')); } check_admin_referer('aizen_save_abilities'); // Checkboxes are only present in $_POST when checked (= enabled). $enabled_raw = isset($_POST['aizen_enabled']) ? (array) wp_unslash($_POST['aizen_enabled']) : array(); $enabled = array_map('sanitize_text_field', $enabled_raw); $all_names = array(); foreach ($this->get_all() as $ability) { $all_names[] = method_exists($ability, 'get_name') ? $ability->get_name() : ''; } $all_names = array_filter($all_names); $disabled = array_values(array_diff($all_names, $enabled)); update_option(AIZEN_OPTION_DISABLED_ABILITIES, $disabled); $this->logger->log( array( 'method' => 'aizen.abilities_updated', 'ability' => '', 'status' => 'success', 'message' => sprintf( /* translators: 1: enabled count, 2: disabled count */ __('%1$d abilities enabled, %2$d disabled by an administrator.', 'abilizen-ai-abilities'), count($enabled), count($disabled) ), ) ); wp_safe_redirect(add_query_arg(array('page' => 'ai-zen', 'tab' => 'abilities', 'updated' => '1'), admin_url('admin.php'))); exit; } /** * Handles the "Enable all" / "Disable all" buttons. */ public function handle_bulk() { if (! current_user_can('manage_options')) { wp_die(esc_html__('You do not have permission to do this.', 'abilizen-ai-abilities')); } check_admin_referer('aizen_bulk_abilities'); $action = isset($_POST['aizen_bulk_action']) ? sanitize_key(wp_unslash($_POST['aizen_bulk_action'])) : ''; if ('disable_all' === $action) { $names = array(); foreach ($this->get_all() as $ability) { $names[] = method_exists($ability, 'get_name') ? $ability->get_name() : ''; } update_option(AIZEN_OPTION_DISABLED_ABILITIES, array_values(array_filter($names))); $message = __('All abilities disabled.', 'abilizen-ai-abilities'); } elseif ('enable_all' === $action) { update_option(AIZEN_OPTION_DISABLED_ABILITIES, array()); $message = __('All abilities enabled.', 'abilizen-ai-abilities'); } else { wp_safe_redirect(add_query_arg(array('page' => 'ai-zen', 'tab' => 'abilities'), admin_url('admin.php'))); exit; } $this->logger->log( array( 'method' => 'aizen.abilities_updated', 'ability' => '', 'status' => 'success', 'message' => $message, ) ); wp_safe_redirect(add_query_arg(array('page' => 'ai-zen', 'tab' => 'abilities', 'updated' => '1'), admin_url('admin.php'))); exit; } public function public_core_abilities(array $args, string $ability_name): array { $core_abilities = array( 'core/get-user-info', 'core/get-site-info', 'core/get-environment-info' ); $disabled = $this->get_disabled(); if (! in_array($ability_name, $core_abilities, true) || in_array($ability_name, $disabled)) { return $args; } $args['meta']['mcp']['public'] = true; if (! empty($args['execute_callback']) && is_callable($args['execute_callback'])) { $original = $args['execute_callback']; $args['execute_callback'] = static function ($input = null) use ($ability_name, $original) { $result = (null === $input) ? $original() : $original($input); return $result; }; } return $args; } }