', esc_attr( self::ROOT_ID ), esc_html__( 'The Quick Connect wizard requires JavaScript. Enable JavaScript in your browser to use it.', 'acrossai-abilities-manager' ) ); } /** * Add the full-screen takeover body class. * * Hooked to `admin_body_class`. * * @since 0.0.34 * @param string $classes Space-separated body classes. * @return string Possibly-extended class list. */ public function add_body_class( $classes ): string { if ( ! $this->is_quick_connect_request() ) { return (string) $classes; } return trim( $classes . ' ' . self::BODY_CLASS ); } /** * Suppress unrelated admin notices while the wizard is on screen. * * Hooked to `in_admin_header` at priority 1000. The CSS takeover hides * WordPress chrome but notices render inside our own content column, so they * must be removed rather than hidden. * * Deliberately request-scoped: without the guard this would strip notices * site-wide, including security-critical ones from core and other plugins. * * @since 0.0.34 * @return void */ public function suppress_admin_notices(): void { if ( ! $this->is_quick_connect_request() ) { return; } remove_all_actions( 'admin_notices' ); remove_all_actions( 'all_admin_notices' ); remove_all_actions( 'user_admin_notices' ); remove_all_actions( 'network_admin_notices' ); } /** * Enqueue the wizard bundle. * * Hooked to `admin_enqueue_scripts`. Gated on the request flag, never the * hook suffix, so the ~200 KB bundle cannot leak onto the abilities table, * the settings page, or the Integrations page (spec FR-042 / SC-010). * * @since 0.0.34 * @return void */ public function enqueue_assets(): void { if ( ! $this->is_quick_connect_request() ) { return; } $build_url = defined( 'ACROSSAI_ABILITIES_MANAGER_PLUGIN_URL' ) ? \ACROSSAI_ABILITIES_MANAGER_PLUGIN_URL . 'build/' : plugins_url( 'build/', dirname( __DIR__, 3 ) . '/acrossai-abilities-manager.php' ); $build_path = defined( 'ACROSSAI_ABILITIES_MANAGER_PLUGIN_PATH' ) ? \ACROSSAI_ABILITIES_MANAGER_PLUGIN_PATH . 'build/' : dirname( __DIR__, 3 ) . '/build/'; $asset_file = $build_path . 'js/quick-connect.asset.php'; // Never include an asset manifest unguarded: a missing or unbuilt bundle // would fatal the whole admin (BUG-UNCONDITIONAL-ASSET-INCLUDE). if ( ! file_exists( $asset_file ) ) { return; } $asset = require $asset_file; $version = (string) ( $asset['version'] ?? '0.0.0' ); wp_enqueue_script( self::HANDLE, $build_url . 'js/quick-connect.js', (array) ( $asset['dependencies'] ?? array() ), $version, true ); wp_set_script_translations( self::HANDLE, 'acrossai-abilities-manager' ); wp_localize_script( self::HANDLE, 'acrossaiQuickConnect', $this->bootstrap_data() ); if ( file_exists( $build_path . 'css/quick-connect.css' ) ) { wp_enqueue_style( self::HANDLE, $build_url . 'css/quick-connect.css', array(), $version ); } } /** * Data handed to the React bundle. * * Data-minimal by design: URLs the client cannot safely build, plus the REST * nonce. No secrets, no PII. * * @since 0.0.34 * @return array Bootstrap payload. */ private function bootstrap_data(): array { $assets_url = defined( 'ACROSSAI_ABILITIES_MANAGER_PLUGIN_URL' ) ? \ACROSSAI_ABILITIES_MANAGER_PLUGIN_URL . 'assets/quick-connect/' : plugins_url( 'assets/quick-connect/', dirname( __DIR__, 3 ) . '/acrossai-abilities-manager.php' ); return array( 'restUrl' => esc_url_raw( rest_url( 'acrossai/v1/quick-connect' ) ), 'restNonce' => wp_create_nonce( 'wp_rest' ), 'adminUrl' => esc_url_raw( admin_url( 'admin.php?page=acrossai-abilities-manager' ) ), 'pluginInstallUrl' => esc_url_raw( admin_url( 'plugin-install.php' ) ), 'logoUrl' => esc_url_raw( $assets_url . 'acrossai-logo.svg' ), 'iconUrl' => esc_url_raw( $assets_url . 'icon.svg' ), 'mcpAdapterRepoUrl' => 'https://github.com/WordPress/mcp-adapter', /* * GitHub's permanent "latest release" alias, so this URL does not go * stale as the adapter releases. * * The wizard sends the operator to this page rather than linking the * asset directly. A direct asset link starts a download on click with * nothing on screen saying what arrived; the release page keeps the * version and the assets list visible, so choosing `mcp-adapter.zip` * over the source archive is a decision the operator can see * themselves making. That choice matters: the source zipball unpacks * to `WordPress-mcp-adapter-`, installing under the wrong folder * name and silently breaking detection and updates. */ 'mcpAdapterReleasesUrl' => 'https://github.com/WordPress/mcp-adapter/releases/latest', 'pluginUploadUrl' => esc_url_raw( admin_url( 'plugin-install.php?tab=upload' ) ), 'pluginsListUrl' => esc_url_raw( admin_url( 'plugins.php' ) ), ); } }