Keyed by integration slug. */ public static function discover(): array { $found = self::from_toolset_integrations(); foreach ( AcrossAI_Ability_Library_Registry::instance()->get_definitions() as $definition ) { $variant = isset( $definition['card_variant'] ) ? (string) $definition['card_variant'] : ''; if ( 'integration' !== $variant ) { continue; } $slug = isset( $definition['category'] ) ? (string) $definition['category'] : ''; if ( '' === $slug ) { continue; } if ( ! isset( $found[ $slug ] ) ) { $label = isset( $definition['category_label'] ) ? (string) $definition['category_label'] : $slug; $found[ $slug ] = array( 'label' => $label, 'count' => 0, ); } ++$found[ $slug ]['count']; } ksort( $found ); return $found; } /** * Integration slugs this site could show a row for, without resolving ability counts. * * Deliberately separate from {@see self::discover()}, which counts. Counting resolves * `AcrossAI_Ability_Group`, and that memoises for the rest of the request — so asking for counts * before `wp_abilities_api_init` has fired caches an empty group map and takes the admin tab * strip's counts down with it. {@see AcrossAI_Integration_Default_Opt_Ins} runs at * `plugins_loaded` P0, long before any ability exists, and needs slugs only. * * Descriptors are safe to ask that early: the registry reads a filter and each integration's own * `is_active()` probe, neither of which touches the ability registry. * * @since 0.0.42 * @return string[] */ public static function candidate_slugs(): array { $slugs = array(); foreach ( AcrossAI_Toolset_Integrations::all() as $group => $integration ) { if ( AcrossAI_Toolset_Integrations::CATCH_ALL_GROUP === $group || ! $integration->is_active() ) { continue; } $slugs[] = (string) $group; } return $slugs; } /** * Rows for every integration whose plugin is active, from the toolset registry. * * The Library definitions this method supplements only describe opt-in integrations — the three * or four hosts that keep their own abilities switched off until asked. Every other integration * is ours and already registered, so it pushed no `card_variant => 'integration'` row and never * appeared on the screen. That is why the tab listed three of twenty-odd. * * Counts come from {@see AcrossAI_Ability_Group::counts()}, which resolves once per request and * is the same source the admin tab strip counts from, so the two cannot disagree. A group with no * registered abilities is skipped: an integration whose plugin is active but which contributes * nothing has nothing to switch. * * @since 0.0.42 * @return array */ private static function from_toolset_integrations(): array { $rows = array(); $counts = AcrossAI_Ability_Group::counts(); foreach ( AcrossAI_Toolset_Integrations::all() as $group => $integration ) { if ( AcrossAI_Toolset_Integrations::CATCH_ALL_GROUP === $group ) { continue; } if ( ! $integration->is_active() ) { continue; } $count = isset( $counts[ $group ] ) ? (int) $counts[ $group ] : 0; if ( 0 === $count ) { continue; } $label = $integration->toolset_label(); $rows[ $group ] = array( 'label' => '' !== $label ? $label : $group, 'count' => $count, ); } return $rows; } /** * Capability required to change a given integration's opt-in. * * A SINGLE filtered `current_user_can()` behind a surface already gated at `manage_options` is * inherently raise-only: a filter can demand more, never less, because the outer gate still * applies (PATTERN-FILTERABLE-CAPABILITY-RAISE-ONLY). Splitting this into two checks, or making * it the only gate, would let a filter returning a weaker capability lower the requirement. * * @since 0.0.34 * @param string $slug Integration slug. * @return string Capability name. */ public static function required_capability( string $slug ): string { /** * Filters the capability required to toggle a third-party integration. * * Raise-only in practice — see the note above. Returning a weaker capability does not grant * access, because the settings page itself is registered at `manage_options`. * * @since 0.1.0 * @param string $capability Default 'manage_options'. * @param string $slug Integration slug. */ return (string) apply_filters( 'acrossai_integration_toggle_capability', 'manage_options', $slug ); } }