prune_expired_new_credentials(); update_option( $this->new_credentials_option_name(), array( 'credentials' => $credentials, 'user_id' => get_current_user_id(), 'expires' => time() + 5 * MINUTE_IN_SECONDS, ), false ); } /** * Delete expired pending-credentials options left by users who * generated credentials but never returned to view them. * * @return void */ private function prune_expired_new_credentials() { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- prefix scan over per-user options has no options-API equivalent. $option_names = $wpdb->get_col( $wpdb->prepare( "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s", $wpdb->esc_like( self::NEW_CREDENTIALS_OPTION . '_' ) . '%' ) ); foreach ( (array) $option_names as $option_name ) { $stored = get_option( $option_name ); if ( ! is_array( $stored ) || time() > (int) ( $stored['expires'] ?? 0 ) ) { delete_option( $option_name ); } } } /** * Fetch and consume newly generated credentials for one-time display. * * Returns the credentials only if they belong to the current user, have * not expired, and match the requested MCP profile. The stored option is * deleted once consumed (or when expired/foreign) so the secret is only * ever shown once. * * @param string $profile Expected MCP profile for the calling section. * @return array|null Credentials array or null if none to display. */ private function take_new_credentials( $profile ) { $option_name = $this->new_credentials_option_name(); $stored = get_option( $option_name ); if ( ! is_array( $stored ) || ! isset( $stored['credentials']['client_id'], $stored['credentials']['client_secret'] ) ) { return null; } // Expired, or (belt-and-braces — the option name is already keyed by // user ID) generated by another user: discard without displaying. if ( time() > (int) ( $stored['expires'] ?? 0 ) || get_current_user_id() !== (int) ( $stored['user_id'] ?? 0 ) ) { delete_option( $option_name ); return null; } $credentials = $stored['credentials']; $stored_profile = $credentials['profile'] ?? self::MCP_PROFILE_ANTHROPIC; // Leave the option in place for the section that matches its profile. if ( $stored_profile !== $profile ) { return null; } delete_option( $option_name ); return $credentials; } /** * Verify the settings page nonce for URL parameters. * * Returns true if nonce is valid, false if no nonce present. * Dies with error if nonce is present but invalid. * * @return bool True if nonce is valid, false if no nonce present. */ private function verify_settings_nonce() { $nonce = filter_input( INPUT_GET, '_wpnonce', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); // No nonce present = no URL parameters to process. if ( empty( $nonce ) ) { return false; } // Nonce present - verify it. if ( ! wp_verify_nonce( $nonce, 'abilities_bridge_settings_nav' ) ) { wp_die( esc_html__( 'Security verification failed. Please use the navigation links provided.', 'abilities-bridge' ), esc_html__( 'Security Check Failed', 'abilities-bridge' ), array( 'response' => 403 ) ); } return true; } /** * Get MCP profile definitions used by settings UI. * * @return array */ private function get_mcp_profiles() { return array( self::MCP_PROFILE_ANTHROPIC => array( 'label' => __( 'Claude Account Connection', 'abilities-bridge' ), 'tab' => 'anthropic-mcp', 'button_label' => __( 'Generate New Claude Connection Credentials', 'abilities-bridge' ), 'intro' => __( 'Connect a Claude account to this WordPress site through MCP and OAuth. This is separate from the Anthropic API key used and billed for the built-in chat.', 'abilities-bridge' ), ), self::MCP_PROFILE_CHATGPT => array( 'label' => __( 'ChatGPT Account Connection', 'abilities-bridge' ), 'tab' => 'chatgpt-mcp', 'button_label' => __( 'Generate New ChatGPT Connection Credentials', 'abilities-bridge' ), 'intro' => __( 'Connect a ChatGPT account to this WordPress site through MCP and OAuth. This is separate from the OpenAI API key used and billed for the built-in chat.', 'abilities-bridge' ), ), ); } /** * Get MCP profile data. * * @param string $profile Requested profile. * @return array */ private function get_mcp_profile( $profile ) { $profiles = $this->get_mcp_profiles(); $profile = sanitize_key( $profile ); if ( isset( $profiles[ $profile ] ) ) { return $profiles[ $profile ]; } return $profiles[ self::MCP_PROFILE_ANTHROPIC ]; } /** * Get the WordPress-hosted MCP endpoint URL. * * The ChatGPT profile appends a ?profile=openai_chatgpt_mcp query argument * so the OpenAI/ChatGPT app builder is permitted to discover actions before * OAuth. Anthropic clients (such as Claude) use the plain endpoint, which * requires authentication and therefore triggers their OAuth flow. * * @param string $profile Optional MCP profile. * @return string */ private function get_mcp_endpoint_url( $profile = '' ) { $url = rest_url( 'abilities-bridge-mcp/v1/mcp' ); if ( self::MCP_PROFILE_CHATGPT === $profile ) { $url = add_query_arg( 'profile', self::MCP_PROFILE_CHATGPT, $url ); } return $url; } /** * Initialize the settings page */ public function init() { add_action( 'admin_init', array( $this, 'register_settings' ) ); add_action( 'admin_init', array( $this, 'handle_mcp_actions' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) ); add_action( 'wp_ajax_abilities_bridge_test_anthropic', array( $this, 'ajax_test_anthropic_connection' ) ); add_action( 'wp_ajax_abilities_bridge_test_openai', array( $this, 'ajax_test_openai_connection' ) ); } /** * Enqueue CSS and JavaScript for settings page * * @param string $hook The current admin page hook. */ public function enqueue_assets( $hook ) { // Only load on settings page. // Hook format: {parent_slug}_page_{menu_slug}. if ( false === strpos( $hook, 'abilities-bridge-settings' ) ) { return; } $settings_css_version = filemtime( ABILITIES_BRIDGE_PLUGIN_DIR . 'admin/css/settings-page.css' ); $settings_js_version = filemtime( ABILITIES_BRIDGE_PLUGIN_DIR . 'admin/js/settings-page.js' ); wp_enqueue_style( 'abilities-bridge-settings-page', ABILITIES_BRIDGE_PLUGIN_URL . 'admin/css/settings-page.css', array(), $settings_css_version ? $settings_css_version : ABILITIES_BRIDGE_VERSION ); wp_enqueue_script( 'abilities-bridge-settings-page', ABILITIES_BRIDGE_PLUGIN_URL . 'admin/js/settings-page.js', array( 'jquery' ), $settings_js_version ? $settings_js_version : ABILITIES_BRIDGE_VERSION, true ); wp_localize_script( 'abilities-bridge-settings-page', 'abilitiesBridgeSettings', array( 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'abilities_bridge_settings' ), 'memoryConsentGiven' => (bool) get_option( 'abilities_bridge_memory_consent', false ), 'i18n' => array( 'copied' => __( 'Copied!', 'abilities-bridge' ), 'copyFailed' => __( 'Failed to copy to clipboard', 'abilities-bridge' ), 'restorePromptConfirm' => __( 'Are you sure you want to restore the default system prompt? Your customizations will be lost.', 'abilities-bridge' ), 'anthropicTestRunning' => __( 'Testing Anthropic connection...', 'abilities-bridge' ), 'anthropicTestSuccess' => __( 'Anthropic connection successful.', 'abilities-bridge' ), 'anthropicTestFailed' => __( 'Anthropic test failed.', 'abilities-bridge' ), 'anthropicTestAjaxError' => __( 'Anthropic test could not be completed. Please try again.', 'abilities-bridge' ), 'openaiTestRunning' => __( 'Testing OpenAI connection...', 'abilities-bridge' ), 'openaiTestSuccess' => __( 'OpenAI connection successful.', 'abilities-bridge' ), 'openaiTestFailed' => __( 'OpenAI test failed.', 'abilities-bridge' ), 'openaiTestAjaxError' => __( 'OpenAI test could not be completed. Please try again.', 'abilities-bridge' ), ), 'defaultSystemPrompt' => Abilities_Bridge_Claude_API::get_default_system_prompt(), ) ); } /** * Register settings */ public function register_settings() { register_setting( 'abilities_bridge_settings', 'abilities_bridge_api_key', array( 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'default' => '', ) ); register_setting( 'abilities_bridge_settings', 'abilities_bridge_openai_api_key', array( 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', 'default' => '', ) ); register_setting( 'abilities_bridge_settings', 'abilities_bridge_enable_chat_bubble', array( 'type' => 'boolean', 'sanitize_callback' => 'rest_sanitize_boolean', 'default' => false, ) ); register_setting( 'abilities_bridge_settings', 'abilities_bridge_enable_image_attachments', array( 'type' => 'boolean', 'sanitize_callback' => 'rest_sanitize_boolean', 'default' => true, ) ); register_setting( 'abilities_bridge_settings', 'abilities_bridge_fable_fallback_enabled', array( 'type' => 'boolean', 'sanitize_callback' => 'rest_sanitize_boolean', 'default' => true, ) ); register_setting( 'abilities_bridge_settings', 'abilities_bridge_use_wp_ai_client', array( 'type' => 'boolean', 'sanitize_callback' => 'rest_sanitize_boolean', 'default' => false, ) ); register_setting( 'abilities_bridge_settings', 'abilities_bridge_enable_memory', array( 'type' => 'boolean', 'sanitize_callback' => 'rest_sanitize_boolean', 'default' => false, ) ); register_setting( 'abilities_bridge_settings', 'abilities_bridge_memory_consent', array( 'type' => 'boolean', 'sanitize_callback' => 'rest_sanitize_boolean', 'default' => false, ) ); add_settings_section( 'abilities_bridge_api_section', __( 'Built-in AI Chat — API Billing', 'abilities-bridge' ), array( $this, 'render_section' ), 'abilities-bridge-settings' ); add_settings_field( 'abilities_bridge_api_key', __( 'Anthropic API Key — Built-in Chat', 'abilities-bridge' ), array( $this, 'render_api_key_field' ), 'abilities-bridge-settings', 'abilities_bridge_api_section' ); add_settings_field( 'abilities_bridge_openai_api_key', __( 'OpenAI API Key — Built-in Chat', 'abilities-bridge' ), array( $this, 'render_openai_api_key_field' ), 'abilities-bridge-settings', 'abilities_bridge_api_section' ); add_settings_field( 'abilities_bridge_use_wp_ai_client', __( 'WP AI Client Keys — Built-in Chat', 'abilities-bridge' ), array( $this, 'render_wp_ai_client_field' ), 'abilities-bridge-settings', 'abilities_bridge_api_section' ); add_settings_field( 'abilities_bridge_enable_chat_bubble', __( 'Chat Bubble', 'abilities-bridge' ), array( $this, 'render_chat_bubble_field' ), 'abilities-bridge-settings', 'abilities_bridge_api_section' ); add_settings_field( 'abilities_bridge_enable_image_attachments', __( 'Image Attachments', 'abilities-bridge' ), array( $this, 'render_image_attachments_field' ), 'abilities-bridge-settings', 'abilities_bridge_api_section' ); add_settings_field( 'abilities_bridge_fable_fallback_enabled', __( 'Fable Refusal Fallback', 'abilities-bridge' ), array( $this, 'render_fable_fallback_field' ), 'abilities-bridge-settings', 'abilities_bridge_api_section' ); // System Prompt setting. register_setting( 'abilities_bridge_settings', 'abilities_bridge_system_prompt', array( 'type' => 'string', 'sanitize_callback' => 'wp_kses_post', 'default' => '', ) ); add_settings_section( 'abilities_bridge_prompt_section', __( 'System Prompt', 'abilities-bridge' ), array( $this, 'render_prompt_section' ), 'abilities-bridge-settings' ); add_settings_field( 'abilities_bridge_system_prompt', __( 'System Prompt', 'abilities-bridge' ), array( $this, 'render_system_prompt_field' ), 'abilities-bridge-settings', 'abilities_bridge_prompt_section' ); // Memory settings (for separate Memory tab). // Memory tool settings. register_setting( 'abilities_bridge_builtin_tools', 'abilities_bridge_enable_memory', array( 'type' => 'boolean', 'sanitize_callback' => 'rest_sanitize_boolean', 'default' => false, ) ); register_setting( 'abilities_bridge_builtin_tools', 'abilities_bridge_memory_consent', array( 'type' => 'boolean', 'sanitize_callback' => array( $this, 'validate_memory_consent' ), 'default' => false, ) ); } /** * Validate memory consent * * Ensures that if memory is being enabled, consent must be given. * * @param bool $value The consent value being saved. * @return bool The validated consent value. */ public function validate_memory_consent( $value ) { // Sanitize the boolean value first. $value = rest_sanitize_boolean( $value ); // Check if memory is being enabled (from POST data). // Settings API sanitize callbacks are called after nonce verification by options.php. $enable_memory_raw = filter_input( INPUT_POST, 'abilities_bridge_enable_memory', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); $enable_memory = rest_sanitize_boolean( $enable_memory_raw ); // If memory is enabled but consent is not given, show error and prevent enabling. if ( $enable_memory && ! $value ) { add_settings_error( 'abilities_bridge_memory_consent', 'memory_consent_required', __( 'You must check the consent box to enable the Memory Tool.', 'abilities-bridge' ), 'error' ); // Also prevent memory from being enabled by resetting it. update_option( 'abilities_bridge_enable_memory', false ); // Return false to prevent consent from being saved as true. return false; } return $value; } /** * Render section description */ public function render_section() { ?>

Anthropic Console.', 'abilities-bridge' ), array( 'a' => array( 'href' => array(), 'target' => array(), ), ) ), esc_url( 'https://console.anthropic.com/' ) ); ?>

' . esc_html( $model ) . '' ); ?>

' . esc_html( $model ) . '' ); ?>

Connectors. If a key is not found there, it falls back to the local keys above.', 'abilities-bridge' ); ?>

[OK] [ - ]
[OK] [ - ]

__( 'Insufficient permissions.', 'abilities-bridge' ) ) ); } $provider = Abilities_Bridge_AI_Provider::PROVIDER_ANTHROPIC; $model = Abilities_Bridge_AI_Provider::get_selected_model( $provider ); if ( ! Abilities_Bridge_AI_Provider::has_api_key( $provider ) ) { wp_send_json_error( array( 'code' => 'no_api_key', 'message' => __( 'Anthropic API key is not configured.', 'abilities-bridge' ), 'model' => $model, ) ); } $anthropic_client = Abilities_Bridge_AI_Provider::create_client( $provider ); $result = $anthropic_client->send_message( array( array( 'role' => 'user', 'content' => 'Return exactly: OK', ), ), array(), 32, $model ); if ( is_wp_error( $result ) ) { $error_data = (array) $result->get_error_data(); wp_send_json_error( array( 'code' => $result->get_error_code(), 'message' => $result->get_error_message(), 'model' => $model, 'provider' => 'anthropic', 'status' => isset( $error_data['status'] ) ? intval( $error_data['status'] ) : null, 'provider_msg' => isset( $error_data['provider_message'] ) ? sanitize_text_field( $error_data['provider_message'] ) : '', ) ); } wp_send_json_success( array( 'message' => __( 'Anthropic test succeeded.', 'abilities-bridge' ), 'model' => $model, ) ); } /** * AJAX: Test OpenAI API connection. */ public function ajax_test_openai_connection() { check_ajax_referer( 'abilities_bridge_settings', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'abilities-bridge' ) ) ); } $provider = Abilities_Bridge_AI_Provider::PROVIDER_OPENAI; $model = Abilities_Bridge_AI_Provider::get_selected_model( $provider ); if ( ! Abilities_Bridge_AI_Provider::has_api_key( $provider ) ) { wp_send_json_error( array( 'code' => 'no_api_key', 'message' => __( 'OpenAI API key is not configured.', 'abilities-bridge' ), 'model' => $model, ) ); } $openai_client = Abilities_Bridge_AI_Provider::create_client( $provider ); $result = $openai_client->send_message( array( array( 'role' => 'user', 'content' => 'Return exactly: OK', ), ), array(), 32, $model ); if ( is_wp_error( $result ) ) { $error_data = $result->get_error_data(); wp_send_json_error( array( 'code' => $result->get_error_code(), 'message' => $result->get_error_message(), 'model' => $model, 'provider' => 'openai', 'status' => isset( $error_data['status'] ) ? intval( $error_data['status'] ) : null, 'provider_msg' => isset( $error_data['provider_message'] ) ? sanitize_text_field( $error_data['provider_message'] ) : '', ) ); } wp_send_json_success( array( 'message' => __( 'OpenAI test succeeded.', 'abilities-bridge' ), 'model' => $model, ) ); } /** * Render system prompt section description */ public function render_prompt_section() { ?>

Database (abilities_bridge_memories table)' ); ?>

memory
/>
get_current_page(); if ( 'abilities-bridge-settings' !== $page ) { return; } // Only process POST requests. $method = isset( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : ''; if ( 'POST' !== $method ) { return; } // Verify capability BEFORE processing any POST data. if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You do not have sufficient permissions to perform this action.', 'abilities-bridge' ), esc_html__( 'Permission Denied', 'abilities-bridge' ), array( 'response' => 403 ) ); } // Check if nonce is present - if not, this isn't an MCP form submission. if ( ! isset( $_POST['_wpnonce'] ) ) { return; } // Handle OAuth credential generation. if ( isset( $_POST['generate_oauth'] ) ) { // Verify nonce BEFORE processing. if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ), 'abilities_bridge_mcp_generate_oauth' ) ) { wp_die( esc_html__( 'Security token validation failed. Please refresh the page and try again.', 'abilities-bridge' ), esc_html__( 'Security Check Failed', 'abilities-bridge' ), array( 'response' => 403 ) ); } // Check if consent has been given. if ( ! Abilities_Bridge_Welcome_Wizard::is_setup_complete() ) { wp_die( esc_html__( 'You must complete the welcome wizard and provide consent before generating OAuth credentials.', 'abilities-bridge' ) ); } $profile = isset( $_POST['mcp_profile'] ) ? sanitize_key( wp_unslash( $_POST['mcp_profile'] ) ) : self::MCP_PROFILE_ANTHROPIC; $oauth = new Abilities_Bridge_MCP_OAuth(); $credentials = $oauth->generate_credentials( null, $profile, array( 'name' => $this->get_mcp_profile( $profile )['label'], 'default_site_url' => home_url( '/' ), ) ); // Store for one-time display after redirect (DB-backed, not a // transient: external object caches can silently drop transients). $this->store_new_credentials( $credentials ); // Redirect to avoid resubmission (include nonce for credentials-generated flag). // Note: wp_nonce_url() must NOT be used here - it calls esc_html() which // converts & to &, breaking query parameters in the Location header. $redirect_url = add_query_arg( array( 'credentials-generated' => '1', 'tab' => $this->get_mcp_profile( $profile )['tab'], '_wpnonce' => wp_create_nonce( 'abilities_bridge_settings_nav' ), ), admin_url( 'admin.php?page=abilities-bridge-settings' ) ); wp_safe_redirect( $redirect_url ); exit; } // Handle client revocation. if ( isset( $_POST['revoke_client'] ) ) { // Verify nonce BEFORE processing. if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ), 'abilities_bridge_mcp_revoke_client' ) ) { wp_die( esc_html__( 'Security token validation failed. Please refresh the page and try again.', 'abilities-bridge' ), esc_html__( 'Security Check Failed', 'abilities-bridge' ), array( 'response' => 403 ) ); } $profile = isset( $_POST['mcp_profile'] ) ? sanitize_key( wp_unslash( $_POST['mcp_profile'] ) ) : self::MCP_PROFILE_ANTHROPIC; $oauth = new Abilities_Bridge_MCP_OAuth(); if ( ! isset( $_POST['client_id'] ) ) { wp_die( esc_html__( 'Missing client ID', 'abilities-bridge' ) ); } $client_id = sanitize_text_field( wp_unslash( $_POST['client_id'] ) ); if ( $oauth->revoke_client( $client_id ) ) { add_settings_error( 'abilities_bridge_messages', 'client_revoked', __( 'Client credentials revoked successfully.', 'abilities-bridge' ), 'success' ); } else { add_settings_error( 'abilities_bridge_messages', 'client_revoke_failed', __( 'Failed to revoke client credentials.', 'abilities-bridge' ), 'error' ); } // Redirect to avoid resubmission. $redirect_url = add_query_arg( array( 'tab' => $this->get_mcp_profile( $profile )['tab'], '_wpnonce' => wp_create_nonce( 'abilities_bridge_settings_nav' ), ), admin_url( 'admin.php?page=abilities-bridge-settings' ) ); wp_safe_redirect( $redirect_url ); exit; } } /** * Render MCP section description and setup instructions. */ public function render_mcp_section() { // Get MCP endpoint URL. $mcp_endpoint = $this->get_mcp_endpoint_url(); // Check for existing OAuth credentials. $oauth = new Abilities_Bridge_MCP_OAuth(); $existing_clients = $oauth->get_user_clients( null, self::MCP_PROFILE_ANTHROPIC ); // Check for newly generated credentials (from transient after redirect). // Verify nonce before accessing the credentials-generated URL parameter. $generated_client_id = null; $generated_client_secret = null; $show_credentials = false; // Only check credentials-generated if nonce is valid. if ( $this->verify_settings_nonce() ) { $credentials_flag = filter_input( INPUT_GET, 'credentials-generated', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); $show_credentials = ! empty( $credentials_flag ); } if ( $show_credentials ) { $credentials = $this->take_new_credentials( self::MCP_PROFILE_ANTHROPIC ); if ( $credentials ) { $generated_client_id = $credentials['client_id']; $generated_client_secret = $credentials['client_secret']; } } ?>

[!] SECURITY NOTICE: ONE-TIME VIEW

  1. Settings -> Connectors', 'abilities-bridge' ); ?>

get_mcp_endpoint_url( self::MCP_PROFILE_CHATGPT ); $oauth = new Abilities_Bridge_MCP_OAuth(); $existing_clients = $oauth->get_user_clients( null, self::MCP_PROFILE_CHATGPT ); $generated_client_id = null; $generated_client_secret = null; $show_credentials = false; if ( $this->verify_settings_nonce() ) { $credentials_flag = filter_input( INPUT_GET, 'credentials-generated', FILTER_SANITIZE_FULL_SPECIAL_CHARS ); $show_credentials = ! empty( $credentials_flag ); } if ( $show_credentials ) { $credentials = $this->take_new_credentials( self::MCP_PROFILE_CHATGPT ); if ( $credentials ) { $generated_client_id = $credentials['client_id']; $generated_client_secret = $credentials['client_secret']; } } ?>

  1. Apps > Advanced Settings and enable developer mode.', 'abilities-bridge' ); ?>

check_requirements(); // Determine which tab should be active (from URL param, defaults to 'general'). $valid_tabs = array( 'general', 'memory', 'anthropic-mcp', 'chatgpt-mcp', 'about', 'pro', 'learn-more' ); $active_tab = isset( $_GET['tab'] ) ? sanitize_text_field( wp_unslash( $_GET['tab'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( ! in_array( $active_tab, $valid_tabs, true ) ) { $active_tab = 'general'; } ?>

[OK] [Missing]
render_builtin_tools_section(); ?>
render_mcp_section(); ?>
render_chatgpt_mcp_section(); ?>

  1. -
  2. -

'PHP Version', 'status' => version_compare( PHP_VERSION, '7.4', '>=' ), 'details' => 'Current: ' . PHP_VERSION . ' (Required: 7.4+)', ); // Check JSON extension. $requirements[] = array( 'name' => 'JSON Extension', 'status' => function_exists( 'json_encode' ), 'details' => function_exists( 'json_encode' ) ? 'Available' : 'Missing - required for API communication', ); // Check cURL extension. $requirements[] = array( 'name' => 'cURL Extension', 'status' => function_exists( 'curl_init' ), 'details' => function_exists( 'curl_init' ) ? 'Available' : 'Missing - required for API communication', ); // Check file permissions for uploads directory using WP_Filesystem. $upload_dir = wp_upload_dir(); $content_dir = $upload_dir['basedir'] . '/abilities-bridge'; global $wp_filesystem; if ( empty( $wp_filesystem ) ) { require_once ABSPATH . 'wp-admin/includes/file.php'; WP_Filesystem(); } $parent_writable = $wp_filesystem->is_writable( $upload_dir['basedir'] ); $dir_exists = $wp_filesystem->is_dir( $content_dir ); $dir_writable = $dir_exists && $wp_filesystem->is_writable( $content_dir ); $requirements[] = array( 'name' => 'Uploads Directory Writable', 'status' => $parent_writable || $dir_writable, 'details' => $parent_writable ? 'Uploads directory writable' : ( $dir_exists ? 'Plugin directory exists' : 'Cannot create plugin directory' ), ); return $requirements; } }