[ 'file' => 'ai/flexa-ai.php', 'plugin' => 'flexa-ai/flexa-ai.php', 'namespace' => 'FlexaAi', ], 'analytics' => [ 'file' => 'analytics/flexa-analytics.php', 'plugin' => 'flexa-analytics/flexa-analytics.php', 'namespace' => 'FlexaAnalytics', ], 'booking' => [ 'file' => 'booking/flexa-booking.php', 'plugin' => 'flexa-booking/flexa-booking.php', 'namespace' => 'FlexaBooking', ], 'crm' => [ 'file' => 'crm/flexa-crm.php', 'plugin' => 'flexa-crm/flexa-crm.php', 'namespace' => 'FlexaCrm', ], 'membership' => [ 'file' => 'membership/flexa-membership.php', 'plugin' => 'flexa-membership/flexa-membership.php', 'namespace' => 'FlexaMembership', ], 'mobile-api' => [ 'file' => 'mobile-api/flexa-mobile-api.php', 'plugin' => 'flexa-mobile-api/flexa-mobile-api.php', 'namespace' => 'FlexaMobileApi', ], 'payments' => [ 'file' => 'payments/flexa-payments.php', 'plugin' => 'flexa-payments/flexa-payments.php', 'namespace' => 'FlexaPayments', ], 'reviews' => [ 'file' => 'reviews/flexa-reviews.php', 'plugin' => 'flexa-reviews/flexa-reviews.php', 'namespace' => 'FlexaReviews', ], ]; /** @var list */ private static array $loaded = []; /** * Include every module that is on. Called from Core's main file, before * `plugins_loaded`, because each add-on boots on that hook. */ public static function load(): void { foreach ( self::to_load() as $slug ) { self::include( $slug ); } } /** * The modules that are on, in boot order. * * @return list */ public static function enabled(): array { $stored = get_option( self::OPTION, [] ); $stored = is_array( $stored ) ? $stored : []; return array_values( array_filter( array_keys( self::MODULES ), static fn( string $slug ): bool => in_array( $slug, $stored, true ) ) ); } /** * The modules this request should include: on, and not also running as a * plugin of their own. * * @return list */ public static function to_load(): array { return array_values( array_filter( self::enabled(), static fn( string $slug ): bool => ! self::standalone_active( $slug ) ) ); } /** * @return list */ public static function loaded(): array { return self::$loaded; } /** * Whether the add-on is active as the plugin it used to be, on this site * or across the network. */ public static function standalone_active( string $slug ): bool { if ( ! isset( self::MODULES[ $slug ] ) ) { return false; } $plugin = self::MODULES[ $slug ]['plugin']; $active = get_option( 'active_plugins', [] ); return ( is_array( $active ) && in_array( $plugin, $active, true ) ) || self::network_active( $plugin ); } /** * Switch a module on: include it and run its Activator (tables, default * settings, anything it set up when it was activated as a plugin). It * boots from the next request, as a freshly activated plugin does. * * False for an unknown module, or one still running as its own plugin. */ public static function enable( string $slug ): bool { if ( ! isset( self::MODULES[ $slug ] ) || self::standalone_active( $slug ) ) { return false; } self::include( $slug ); self::run( $slug, 'Activator', 'activate' ); $enabled = self::enabled(); if ( ! in_array( $slug, $enabled, true ) ) { $enabled[] = $slug; update_option( self::OPTION, $enabled ); } return true; } /** * Switch a module off: run its Deactivator (its cron events) and stop * including it. Its tables and settings stay, as they did when the plugin * was deactivated. */ public static function disable( string $slug ): bool { $enabled = self::enabled(); if ( ! in_array( $slug, $enabled, true ) ) { return false; } if ( in_array( $slug, self::$loaded, true ) ) { self::run( $slug, 'Deactivator', 'deactivate' ); } update_option( self::OPTION, array_values( array_diff( $enabled, [ $slug ] ) ) ); return true; } /** * Move a site off the add-ons it runs as plugins of their own: each one's * module is switched on and the plugin is deactivated quietly, so its * Deactivator does not run and its cron events, tables and settings carry * over. From the next request the module runs in the plugin's place, on * the same data. * * Left alone, and reported as kept: * - a plugin newer than the copy Core carries, which would otherwise be * replaced by older code over data it may already have migrated; * - a plugin active across a network, since modules are switched on per * site and deactivating it would take it from every other site. * * @return array{taken:list,kept:list} */ public static function take_over(): array { $result = [ 'taken' => [], 'kept' => [], ]; foreach ( array_keys( self::MODULES ) as $slug ) { if ( ! self::standalone_active( $slug ) ) { continue; } $plugin = self::MODULES[ $slug ]['plugin']; if ( self::network_active( $plugin ) || version_compare( self::standalone_version( $slug ), self::bundled_version( $slug ), '>' ) ) { $result['kept'][] = $slug; continue; } // The module is switched on, and read back past the options cache, // before the plugin goes: if it did not stick, the plugin stays on // rather than leaving the site with neither. $enabled = self::stored_enabled(); if ( ! in_array( $slug, $enabled, true ) ) { $enabled[] = $slug; update_option( self::OPTION, $enabled ); } if ( ! in_array( $slug, self::stored_enabled(), true ) ) { $result['kept'][] = $slug; continue; } deactivate_plugins( $plugin, true ); $result['taken'][] = $slug; } return $result; } /** * The modules that are on, as the database has them now rather than as * this request first read them. * * @return list */ private static function stored_enabled(): array { wp_cache_delete( self::OPTION, 'options' ); wp_cache_delete( 'alloptions', 'options' ); wp_cache_delete( 'notoptions', 'options' ); return self::enabled(); } /** * The version in the header of the copy under addons/. */ public static function bundled_version( string $slug ): string { $data = get_file_data( FXLC_PATH . 'addons/' . self::MODULES[ $slug ]['file'], [ 'Version' => 'Version' ] ); return (string) ( $data['Version'] ?? '' ); } /** * The version in the header of the plugin of its own, '' when unknown. */ public static function standalone_version( string $slug ): string { $data = get_file_data( WP_PLUGIN_DIR . '/' . self::MODULES[ $slug ]['plugin'], [ 'Version' => 'Version' ] ); return (string) ( $data['Version'] ?? '' ); } private static function network_active( string $plugin ): bool { if ( ! is_multisite() ) { return false; } $network = get_site_option( 'active_sitewide_plugins', [] ); return is_array( $network ) && isset( $network[ $plugin ] ); } /** * Core was activated: the modules that are on get the activation they * would have had as plugins. */ public static function activate_enabled(): void { foreach ( self::to_load() as $slug ) { self::include( $slug ); self::run( $slug, 'Activator', 'activate' ); } } /** * Core was deactivated: the modules that ran get their Deactivator, so no * cron event of theirs is left behind. Which modules are on is kept. */ public static function deactivate_loaded(): void { foreach ( self::$loaded as $slug ) { self::run( $slug, 'Deactivator', 'deactivate' ); } } private static function include( string $slug ): void { if ( in_array( $slug, self::$loaded, true ) ) { return; } require_once FXLC_PATH . 'addons/' . self::MODULES[ $slug ]['file']; self::$loaded[] = $slug; } /** * Call an add-on's Install class if it has one (AI has no Deactivator). */ private static function run( string $slug, string $class_name, string $method ): void { $class = '\\' . self::MODULES[ $slug ]['namespace'] . '\\Install\\' . $class_name; if ( class_exists( $class ) && is_callable( [ $class, $method ] ) ) { call_user_func( [ $class, $method ] ); } } }