*/ final class Main { /** * The single instance of the class. * * @var AcrossAI_Abilities_Manager * @since 0.0.1 */ protected static $instance = null; /** * The autoloader instance. * * @since 0.0.1 * @access protected * @var Autoloader $autoloader The plugin autoloader instance. */ protected $autoloader; /** * The loader that's responsible for maintaining and registering all hooks that power * the plugin. * * @since 0.0.1 * @access protected * @var AcrossAI_Abilities_Manager_Loader $loader Maintains and registers all hooks for the plugin. */ protected $loader; /** * The unique identifier of this plugin. * * @since 0.0.1 * @access protected * @var string $plugin_name The string used to uniquely identify this plugin. */ protected $plugin_name; /** * The plugin dir path * * @since 0.0.1 * @access protected * @var string $plugin_path The string for plugin dir path */ protected $plugin_path; /** * The current version of the plugin. * * @since 0.0.1 * @access protected * @var string $version The current version of the plugin. */ protected $version; /** * Plugin directory path. * * @var string */ protected $plugin_dir; /** * Boot-resilience flag set by load_composer_dependencies() when * vendor/autoload_packages.php is absent. When true, __construct() * registers a manage_options-gated admin notice and returns before * load_dependencies() — preventing the AcrossAI_Loader class-not-found * fatal at line 228 that was the reproducer for Feature 038 (Constitution * §V Integration Resilience). * * @since 0.0.1 * @var bool */ private bool $vendor_missing = false; /** * Define the core functionality of the plugin. * * Set the plugin name and the plugin version that can be used throughout the plugin. * Load the dependencies and set the hooks for the admin area and * the public-facing side of the site. * * @since 0.0.1 */ private function __construct() { $this->define_constants(); $this->plugin_name = 'acrossai-abilities-manager'; $this->version = ACROSSAI_ABILITIES_MANAGER_VERSION; // Load the autoloader class manually before registering it. $this->plugin_dir = ACROSSAI_ABILITIES_MANAGER_PLUGIN_PATH; $this->load_composer_dependencies(); // Feature 038 boot resilience (T011 / SEC-001 / DEC-FAIL-OPEN-NOTICE): // When vendor/autoload_packages.php is absent the PSR-4 autoloader // is not registered, so referencing any plugin-namespaced class // would fatal. Register a self-contained admin notice (closure // uses only WP globals — no $this->, no FQCN under // AcrossAI_Abilities_Manager\…) and short-circuit before // load_dependencies() and load_hooks(). if ( $this->vendor_missing ) { add_action( 'admin_notices', static function () { if ( ! current_user_can( 'manage_options' ) ) { return; } printf( '

%s %s

', esc_html__( 'AcrossAI Abilities Manager:', 'acrossai-abilities-manager' ), esc_html__( 'The Composer autoloader is missing. Run "composer install" inside the plugin directory to restore functionality.', 'acrossai-abilities-manager' ) ); } ); return; } $this->load_dependencies(); $this->load_hooks(); } /** * Main AcrossAI_Abilities_Manager Instance. * * Ensures only one instance of WooCommerce is loaded or can be loaded. * * @since 0.0.1 * @static * @see AcrossAI_Abilities_Manager() * @return AcrossAI_Abilities_Manager - Main instance. */ public static function instance() { if ( null === self::$instance ) { self::$instance = new self(); } return self::$instance; } /** * Define WCE Constants */ private function define_constants() { $this->define( 'ACROSSAI_ABILITIES_MANAGER_PLUGIN_BASENAME', plugin_basename( \ACROSSAI_ABILITIES_MANAGER_PLUGIN_FILE ) ); $this->define( 'ACROSSAI_ABILITIES_MANAGER_PLUGIN_PATH', plugin_dir_path( \ACROSSAI_ABILITIES_MANAGER_PLUGIN_FILE ) ); $this->define( 'ACROSSAI_ABILITIES_MANAGER_PLUGIN_URL', plugin_dir_url( \ACROSSAI_ABILITIES_MANAGER_PLUGIN_FILE ) ); $this->define( 'ACROSSAI_ABILITIES_MANAGER_PLUGIN_NAME_SLUG', $this->plugin_name ); $this->define( 'ACROSSAI_ABILITIES_MANAGER_PLUGIN_NAME', 'AcrossAI Abilities Manager' ); $this->define( 'ACROSSAI_ABILITIES_MANAGER_VERSION', '0.0.8' ); } /** * Define constant if not already set. * * @param string $acrossai_name The constant name. * @param string|bool $acrossai_value The constant value. */ private function define( $acrossai_name, $acrossai_value ) { if ( ! defined( $acrossai_name ) ) { define( $acrossai_name, $acrossai_value ); } } /** * Register all the hook once all the active plugins are loaded * * Uses the plugins_loaded to load all the hooks and filters * * @since 0.0.1 * @access private */ public function load_hooks() { /** * Check if plugin can be loaded safely or not * * @since 0.0.1 */ if ( apply_filters( 'acrossai_abilities_manager_load', true ) ) { $this->define_admin_hooks(); $this->define_public_hooks(); } } /** * Load the required composer dependencies for this plugin. * * @since 0.0.1 * @access private */ private function load_composer_dependencies() { /** * Add composer file */ $plugin_path = ACROSSAI_ABILITIES_MANAGER_PLUGIN_PATH; if ( file_exists( $plugin_path . 'vendor/autoload_packages.php' ) ) { require_once $plugin_path . 'vendor/autoload_packages.php'; return; } // Feature 038 boot resilience: signal __construct() to short-circuit // before load_dependencies() touches the PSR-4 autoloader. Surfaced // to admins via the manage_options-gated admin notice in __construct(). $this->vendor_missing = true; } /** * Load the required dependencies for this plugin. * * Include the following files that make up the plugin: * * - AcrossAI_Abilities_Manager\Admin\Loader. Orchestrates the hooks of the plugin. * - AcrossAI_Abilities_Manager\Admin\Main. Defines all hooks for the admin area. * - AcrossAI_Abilities_Manager_Public. Defines all hooks for the public side of the site. * * Create an instance of the loader which will be used to register the hooks * with WordPress. * * @since 0.0.1 * @access private */ private function load_dependencies() { $this->loader = AcrossAI_Loader::instance(); } /** * Register all of the hooks related to the admin area functionality * of the plugin. * * @since 0.0.1 * @access private */ private function define_admin_hooks() { $plugin_admin = new \AcrossAI_Abilities_Manager\Admin\Main( $this->get_plugin_name(), $this->get_version() ); $this->loader->add_action( 'admin_enqueue_scripts', $plugin_admin, 'enqueue_styles' ); $this->loader->add_action( 'admin_enqueue_scripts', $plugin_admin, 'enqueue_scripts' ); $this->loader->add_action( 'plugin_action_links', $plugin_admin, 'plugin_action_links', 1000, 2 ); // Feature 053: remove THIS plugin's own entry from the shared // `acrossai_addons` list rendered by the acrossai-co/main-menu Add-ons // page. This plugin is obviously active when the page renders — no // point offering to install itself. $this->loader->add_filter( 'acrossai_addons', $plugin_admin, 'filter_out_self_from_addons' ); /** * Add the Plugin Main Menu */ $main_menu = new \AcrossAI_Abilities_Manager\Admin\Partials\Menu( $this->get_plugin_name(), $this->get_version() ); $this->loader->add_action( 'admin_menu', $main_menu, 'register_submenu' ); // Library submenu page — second position, immediately after main menu (Feature 027/031). // Named variable before Loader call — Boot Flow Rule variable-first pattern. $ability_library_menu = \AcrossAI_Abilities_Manager\Admin\Partials\LibraryMenu::instance(); $this->loader->add_action( 'admin_menu', $ability_library_menu, 'register_submenu' ); // Settings sections (Feature 019; reparented to host page in Feature 038; // migrated to instance-method API + tab-scoped option_group in Feature 045). // Host page rendering owned by acrossai-co/main-menu — the plugin // only contributes (a) an "Abilities" tab via the // `acrossai_settings_tabs` filter, and (b) settings sections // targeting the per-tab page slug derived by // SettingsPage::get_settings_renderer()->tab_page_slug() (main-menu v0.0.14+). // option_group is the SAME tab-scoped slug, so each tab has its own // whitelist — preventing the cross-tab option-clobber bug that // shared-`acrossai-settings` had in 0.0.12. // Named variable before Loader call — Boot Flow Rule variable-first pattern (AC-HOOKS-MAIN). $settings_menu = \AcrossAI_Abilities_Manager\Admin\Partials\SettingsMenu::instance(); $this->loader->add_filter( 'acrossai_settings_tabs', $settings_menu, 'register_tab' ); $this->loader->add_action( 'admin_init', $settings_menu, 'register_settings' ); // Feature 046 — Absorbed Core Settings field (extra MIME types) renders // INSIDE the existing Abilities tab. Core_Settings_Menu does not register // its own tab and does not register a second uninstall-opt-in; the // manager's SettingsMenu owns both concerns. Variable-first per AC-HOOKS-MAIN. $core_settings_menu = \AcrossAI_Abilities_Manager\Admin\Partials\Core_Settings_Menu::instance(); $this->loader->add_action( 'admin_init', $core_settings_menu, 'register_settings' ); // Uninstall Settings section is wired LAST — priority 20 pushes it below // every default-priority section (Display Settings above, plus // Core_Settings_Menu's Upload Media Abilities section). Sections render // in the order they were added, so a later priority puts this at the // bottom of the Abilities tab. $this->loader->add_action( 'admin_init', $settings_menu, 'register_uninstall_settings', 20 ); // Add-ons submenu page (Feature 026 → 030 → 039 → 053). // As of acrossai-co/main-menu 0.0.21 the `\AcrossAI_Addon\AddonsPage` // entry-class is removed and the Add-ons submenu is registered // internally by `\AcrossAI_Main_Menu\MenuRegistrar` when the shared // `\AcrossAI_Main_Menu\SettingsPage` bootstrap runs (done at // `plugins_loaded @ P0` in this plugin's entry file, see // acrossai-abilities-manager.php). Consumers extend the shared list // via `add_filter( 'acrossai_addons', … )`; freemius integration was // dropped upstream in 0.0.21 so no `fs_*` args are passed anymore. // The submenu URL admin.php?page=acrossai-addons is unchanged. // Abilities DB table setup — BerlinDB hooks maybe_upgrade() to admin_init. // Named variable before Loader call — Boot Flow Rule variable-first pattern (AC-HOOKS-MAIN). $abilities_table = \AcrossAI_Abilities_Manager\Includes\Modules\Abilities\Database\AcrossAI_Abilities_Table::instance(); // Register Access Control REST routes and admin notice for absent library (SAC-01). $abilities_ac = \AcrossAI_Abilities_Manager\Includes\Modules\Abilities\AcrossAI_Abilities_Access_Control::instance(); $this->loader->add_action( 'rest_api_init', $abilities_ac, 'register_rest_api' ); $this->loader->add_action( 'admin_notices', $abilities_ac, 'maybe_show_library_notice' ); // Abilities module REST routes (Spec 009). // Named variable before Loader call — Boot Flow Rule variable-first pattern. $abilities_rest = \AcrossAI_Abilities_Manager\Includes\Modules\Abilities\Rest\AcrossAI_Abilities_Rest_Controller::instance(); $this->loader->add_action( 'rest_api_init', $abilities_rest, 'register_routes' ); // Library Registry — collect add-on definitions at init P99 (Feature 027). // Named variable before Loader call — Boot Flow Rule variable-first pattern. $ability_library_registry = \AcrossAI_Abilities_Manager\Includes\Modules\Library\AcrossAI_Ability_Library_Registry::instance(); $this->loader->add_action( 'init', $ability_library_registry, 'collect', 99 ); // Library REST orchestrator — acrossai-abilities-library/v1 namespace (Feature 027). // Named variable before Loader call — Boot Flow Rule variable-first pattern. $ability_library_rest = \AcrossAI_Abilities_Manager\Includes\Modules\Library\Rest\AcrossAI_Ability_Library_Rest_Controller::instance(); $this->loader->add_action( 'rest_api_init', $ability_library_rest, 'register_routes' ); } /** * Register all of the hooks related to the public-facing functionality * of the plugin. * * @since 0.0.1 * @access private */ private function define_public_hooks() { $plugin_public = new \AcrossAI_Abilities_Manager\Front\Main( $this->get_plugin_name(), $this->get_version() ); $this->loader->add_action( 'wp_enqueue_scripts', $plugin_public, 'enqueue_styles' ); $this->loader->add_action( 'wp_enqueue_scripts', $plugin_public, 'enqueue_scripts' ); // Ability Override Processor — boot at plugins_loaded P20 and bust cache on override save. // Named variable before Loader calls satisfies the Boot Flow Rule (SEC-PLAN-002). $override_processor = \AcrossAI_Abilities_Manager\Includes\Modules\Abilities\AcrossAI_Ability_Override_Processor::instance(); $this->loader->add_action( 'plugins_loaded', $override_processor, 'boot_hook', 20 ); // CRITICAL-01 fix (sub-change 11d): wire bust_cache_hook to all three Abilities lifecycle hooks. $this->loader->add_action( 'acrossai_abilities_after_create', $override_processor, 'bust_cache_hook' ); $this->loader->add_action( 'acrossai_abilities_after_update', $override_processor, 'bust_cache_hook' ); $this->loader->add_action( 'acrossai_abilities_after_delete', $override_processor, 'bust_cache_hook' ); // Abilities Processor — registers published source=db abilities at wp_abilities_api_init P10 (Spec 009). // Named variable before Loader call — Boot Flow Rule variable-first pattern. $abilities_processor = \AcrossAI_Abilities_Manager\Includes\Modules\Abilities\AcrossAI_Abilities_Processor::instance(); $this->loader->add_action( 'wp_abilities_api_init', $abilities_processor, 'register_abilities', 10 ); // Library Processor — registers add-on abilities at wp_abilities_api_init P5 (Feature 027). // Runs before the DB processor at P10 so add-on abilities are gated first. // Named variable before Loader call — Boot Flow Rule variable-first pattern. $ability_library_processor = \AcrossAI_Abilities_Manager\Includes\Modules\Library\AcrossAI_Ability_Library_Processor::instance(); $this->loader->add_action( 'wp_abilities_api_init', $ability_library_processor, 'register_abilities', 5 ); // Feature 046 — Absorbed Core Abilities Bootstrap. Registers 17 category // callbacks with the Loader (each is one $this->loader->add_action on // wp_abilities_api_categories_init) and hooks the 176-class ability // instantiation to plugins_loaded @ P20 matching the companion plugin's // original hook point. See specs/046-absorb-core-abilities-into-manager/. $core_abilities_bootstrap = \AcrossAI_Abilities_Manager\Includes\Abilities\AcrossAI_Core_Abilities_Bootstrap::instance(); $core_abilities_bootstrap->register_category_callbacks( $this->loader ); $this->loader->add_action( 'plugins_loaded', $core_abilities_bootstrap, 'register_abilities', 20 ); } /** * Run the loader to execute all of the hooks with WordPress. * * @since 0.0.1 */ public function run() { $this->loader->run(); } /** * The name of the plugin used to uniquely identify it within the context of * WordPress and to define internationalization functionality. * * @since 0.0.1 * @return string The name of the plugin. */ public function get_plugin_name() { return $this->plugin_name; } /** * The reference to the class that orchestrates the hooks with the plugin. * * @since 0.0.1 * @return AcrossAI_Abilities_Manager_Loader Orchestrates the hooks of the plugin. */ public function get_loader() { return $this->loader; } /** * The reference to the autoloader instance. * * @since 0.0.1 * @return Autoloader The plugin autoloader instance. */ public function get_autoloader() { return $this->autoloader; } /** * Retrieve the version number of the plugin. * * @since 0.0.1 * @return string The version number of the plugin. */ public function get_version() { return $this->version; } }