plugin_name = ACROSSAI_MCP_MANAGER_PLUGIN_NAME_SLUG; $this->version = ACROSSAI_MCP_MANAGER_VERSION; // Asset manifest reads are deferred to enqueue_*() — see notes there // for the file_exists() guard (FR-019) and the screen-ID guard (FR-017). } private function is_plugin_admin_screen(): bool { if ( ! function_exists( 'get_current_screen' ) ) { return false; } $screen = get_current_screen(); if ( ! $screen ) { return false; } return in_array( $screen->id, AdminPageSlugs::plugin_screen_ids(), true ); } /** * Lazy-load an asset manifest. Returns null when the file is missing * so callers can silently skip enqueue (FR-019). * * @return array{dependencies: string[], version: string}|null */ private function read_asset_manifest( string $relative_path ): ?array { $path = \ACROSSAI_MCP_MANAGER_PLUGIN_PATH . $relative_path; if ( ! file_exists( $path ) ) { return null; } $asset = include $path; if ( ! is_array( $asset ) || ! isset( $asset['version'], $asset['dependencies'] ) ) { return null; } return $asset; } /** * Enqueue backend.css on plugin admin pages only. * * Wired on `admin_enqueue_scripts` by Includes\Main::define_admin_hooks(). */ public function enqueue_styles(): void { if ( ! $this->is_plugin_admin_screen() ) { return; } $asset = $this->read_asset_manifest( 'build/css/backend.asset.php' ); if ( null === $asset ) { return; } wp_enqueue_style( $this->plugin_name, esc_url( \ACROSSAI_MCP_MANAGER_PLUGIN_URL . 'build/css/backend.css' ), $asset['dependencies'], $asset['version'], 'all' ); } /** * Enqueue backend.js on plugin admin pages only. * * Wired on `admin_enqueue_scripts` by Includes\Main::define_admin_hooks(). */ public function enqueue_scripts(): void { if ( ! $this->is_plugin_admin_screen() ) { return; } $asset = $this->read_asset_manifest( 'build/js/backend.asset.php' ); if ( null === $asset ) { return; } wp_enqueue_script( $this->plugin_name, esc_url( \ACROSSAI_MCP_MANAGER_PLUGIN_URL . 'build/js/backend.js' ), $asset['dependencies'], $asset['version'], true // load in footer ); // F015 — Access Control tab React app (vendor's component). // Only enqueue on the per-server-edit page with tab=access-control so we // don't ship the React bundle on unrelated screens. $this->maybe_enqueue_access_control_app(); // F017 — Abilities tab React app (@wordpress/dataviews). // Scoped to the Abilities tab only — same guard shape as F015. $this->maybe_enqueue_abilities_app(); // F020 — Tools tab React app (hand-rolled shuttle picker). // Scoped to the Tools tab only — same guard shape as F015/F017. $this->maybe_enqueue_tools_app(); $this->maybe_enqueue_ai_connectors_app(); } /** * Enqueue the vendor AccessControl React app on the Access Control tab. * * @since 0.0.7 * @return void */ private function maybe_enqueue_access_control_app(): void { // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only routing check. $is_edit = isset( $_GET['action'] ) && 'edit' === sanitize_key( wp_unslash( $_GET['action'] ) ); $is_ac = isset( $_GET['tab'] ) && 'access-control' === sanitize_key( wp_unslash( $_GET['tab'] ) ); if ( ! $is_edit || ! $is_ac ) { return; } // phpcs:enable $asset = $this->read_asset_manifest( 'build/js/access-control.asset.php' ); if ( null === $asset ) { return; } $handle = $this->plugin_name . '-access-control'; wp_enqueue_script( $handle, esc_url( \ACROSSAI_MCP_MANAGER_PLUGIN_URL . 'build/js/access-control.js' ), $asset['dependencies'], $asset['version'], true ); // Vendor CSS bundled alongside the JS entry (webpack emits alongside // the .js/.asset.php). Same handle so the style is deregistered when // the script is. wp_enqueue_style( $handle, esc_url( \ACROSSAI_MCP_MANAGER_PLUGIN_URL . 'build/js/access-control.css' ), array(), $asset['version'] ); // phpcs:disable WordPress.Security.NonceVerification.Recommended $server_id = isset( $_GET['server'] ) ? absint( wp_unslash( $_GET['server'] ) ) : 0; // phpcs:enable $server_slug = ''; if ( $server_id > 0 ) { $rows = \AcrossAI_MCP_Manager\Includes\Database\MCPServer\Query::instance()->query( array( 'id' => $server_id, 'number' => 1, ) ); if ( ! empty( $rows ) ) { $server_slug = (string) $rows[0]->server_slug; } } wp_localize_script( $handle, 'acrossaiMcpAccessControl', array( 'pluginSlug' => \AcrossAI_MCP_Manager\Includes\AccessControl\AcrossAI_MCP_Access_Control::TABLE_SLUG, 'namespace' => 'acrossai-mcp-manager', 'resourceKey' => $server_slug, // The vendor's React component concatenates restApiRoot + '/wpb-ac/…'. // `rest_url()` returns with a trailing slash, which would produce // `/wp-json//wpb-ac/…` → 404. Strip the trailing slash here. 'restApiRoot' => esc_url_raw( untrailingslashit( rest_url() ) ), 'nonce' => wp_create_nonce( 'wp_rest' ), ) ); } /** * Enqueue the F017 Abilities tab React app on the Abilities tab only. * * Mirrors the F015 `maybe_enqueue_access_control_app()` shape verbatim — * `?action=edit` + `?tab=abilities` guard, silent bail on missing asset * manifest (FR-019), localize the `acrossaiMcpAbilities` config. * * @since 0.1.0 * @return void */ private function maybe_enqueue_abilities_app(): void { // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only routing check. $is_edit = isset( $_GET['action'] ) && 'edit' === sanitize_key( wp_unslash( $_GET['action'] ) ); $is_abilities = isset( $_GET['tab'] ) && 'abilities' === sanitize_key( wp_unslash( $_GET['tab'] ) ); if ( ! $is_edit || ! $is_abilities ) { return; } // phpcs:enable $asset = $this->read_asset_manifest( 'build/js/abilities.asset.php' ); if ( null === $asset ) { return; } $handle = $this->plugin_name . '-abilities'; wp_enqueue_script( $handle, esc_url( \ACROSSAI_MCP_MANAGER_PLUGIN_URL . 'build/js/abilities.js' ), $asset['dependencies'], $asset['version'], true ); // SCSS is optional — emit a matching stylesheet only if webpack // produced `build/js/abilities.css` alongside the JS. $css_path = \ACROSSAI_MCP_MANAGER_PLUGIN_PATH . 'build/js/abilities.css'; if ( file_exists( $css_path ) ) { wp_enqueue_style( $handle, esc_url( \ACROSSAI_MCP_MANAGER_PLUGIN_URL . 'build/js/abilities.css' ), array(), $asset['version'] ); } // phpcs:disable WordPress.Security.NonceVerification.Recommended $server_id = isset( $_GET['server'] ) ? absint( wp_unslash( $_GET['server'] ) ) : 0; // phpcs:enable $server_slug = ''; if ( $server_id > 0 ) { $rows = \AcrossAI_MCP_Manager\Includes\Database\MCPServer\Query::instance()->query( array( 'id' => $server_id, 'number' => 1, ) ); if ( ! empty( $rows ) ) { $server_slug = (string) $rows[0]->server_slug; } } wp_localize_script( $handle, 'acrossaiMcpAbilities', array( 'serverId' => $server_id, 'serverSlug' => $server_slug, // B17 defense — `rest_url()` returns with a trailing slash; // the client concatenates `restApiRoot + '/acrossai-mcp-manager/v1/…'` // so we strip the slash here to avoid `//`-doubled routes → 404. 'restApiRoot' => esc_url_raw( untrailingslashit( rest_url() ) ), 'nonce' => wp_create_nonce( 'wp_rest' ), 'namespace' => 'acrossai-mcp-manager/v1', ) ); } /** * Enqueue the F020 Tools tab React shuttle picker on the Tools tab. * * Mirrors `maybe_enqueue_abilities_app()` verbatim — `?action=edit` + * `?tab=tools` guard, silent bail on missing asset manifest (FR-019), * localize `window.acrossaiMcpTools`. * * @since 0.1.0 * @return void */ private function maybe_enqueue_tools_app(): void { // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only routing check. $is_edit = isset( $_GET['action'] ) && 'edit' === sanitize_key( wp_unslash( $_GET['action'] ) ); $is_tools = isset( $_GET['tab'] ) && 'tools' === sanitize_key( wp_unslash( $_GET['tab'] ) ); if ( ! $is_edit || ! $is_tools ) { return; } // phpcs:enable $asset = $this->read_asset_manifest( 'build/js/tools.asset.php' ); if ( null === $asset ) { return; } $handle = $this->plugin_name . '-tools'; wp_enqueue_script( $handle, esc_url( \ACROSSAI_MCP_MANAGER_PLUGIN_URL . 'build/js/tools.js' ), $asset['dependencies'], $asset['version'], true ); // Optional CSS — auto-extracted if src/scss/tools.scss is imported. $css_path = \ACROSSAI_MCP_MANAGER_PLUGIN_PATH . 'build/js/tools.css'; if ( file_exists( $css_path ) ) { wp_enqueue_style( $handle, esc_url( \ACROSSAI_MCP_MANAGER_PLUGIN_URL . 'build/js/tools.css' ), array(), $asset['version'] ); } // phpcs:disable WordPress.Security.NonceVerification.Recommended $server_id = isset( $_GET['server'] ) ? absint( wp_unslash( $_GET['server'] ) ) : 0; // phpcs:enable $server_slug = ''; if ( $server_id > 0 ) { $rows = \AcrossAI_MCP_Manager\Includes\Database\MCPServer\Query::instance()->query( array( 'id' => $server_id, 'number' => 1, ) ); if ( ! empty( $rows ) ) { $server_slug = (string) $rows[0]->server_slug; } } wp_localize_script( $handle, 'acrossaiMcpTools', array( 'serverId' => $server_id, 'serverSlug' => $server_slug, 'restApiRoot' => esc_url_raw( untrailingslashit( rest_url() ) ), 'nonce' => wp_create_nonce( 'wp_rest' ), 'namespace' => 'acrossai-mcp-manager/v1', ) ); } /** * Enqueue the F021 Phase 9 AI Connectors shared card shell on the * AI Connectors tab only. Ships CSS + delegated JS that every companion * connector plugin inherits — companions themselves ship zero assets. * * Mirrors `maybe_enqueue_tools_app()` verbatim for consistency. * * @since 0.1.0 (Phase 9) * @return void */ private function maybe_enqueue_ai_connectors_app(): void { // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only routing check. $is_edit = isset( $_GET['action'] ) && 'edit' === sanitize_key( wp_unslash( $_GET['action'] ) ); $is_ai_connectors = isset( $_GET['tab'] ) && 'ai-connectors' === sanitize_key( wp_unslash( $_GET['tab'] ) ); if ( ! $is_edit || ! $is_ai_connectors ) { return; } // phpcs:enable $asset = $this->read_asset_manifest( 'build/js/ai-connectors.asset.php' ); if ( null === $asset ) { return; } $handle = $this->plugin_name . '-ai-connectors'; wp_enqueue_script( $handle, esc_url( \ACROSSAI_MCP_MANAGER_PLUGIN_URL . 'build/js/ai-connectors.js' ), $asset['dependencies'], $asset['version'], true ); $css_path = \ACROSSAI_MCP_MANAGER_PLUGIN_PATH . 'build/js/ai-connectors.css'; if ( file_exists( $css_path ) ) { wp_enqueue_style( $handle, esc_url( \ACROSSAI_MCP_MANAGER_PLUGIN_URL . 'build/js/ai-connectors.css' ), array(), $asset['version'] ); } wp_localize_script( $handle, 'acrossaiMcpConnectors', array( 'restEndpoint' => esc_url_raw( rest_url( 'acrossai-mcp-manager/v1/oauth/generate-client' ) ), 'namespace' => 'acrossai-mcp-manager/v1', 'copied' => __( 'Copied!', 'acrossai-mcp-manager' ), 'reveal' => __( 'Reveal', 'acrossai-mcp-manager' ), 'hide' => __( 'Hide', 'acrossai-mcp-manager' ), 'working' => __( 'Generating credentials…', 'acrossai-mcp-manager' ), 'failed' => __( 'Failed to generate credentials. Please try again.', 'acrossai-mcp-manager' ), 'missingCtx' => __( 'Missing server context. Reload the page and try again.', 'acrossai-mcp-manager' ), 'confirmRegenerate' => __( 'Regenerating will revoke every outstanding token for this connector. Continue?', 'acrossai-mcp-manager' ), 'issued' => __( 'Credentials generated', 'acrossai-mcp-manager' ), 'clientId' => __( 'OAuth Client ID', 'acrossai-mcp-manager' ), 'secret' => __( 'OAuth Client Secret (visible once — copy it now)', 'acrossai-mcp-manager' ), 'setup' => __( 'Setup instructions', 'acrossai-mcp-manager' ), 'copy' => __( 'Copy', 'acrossai-mcp-manager' ), ) ); } }