modules = new ModuleRegistry(); $this->init_hooks(); } /** * Return (or create) the single Plugin instance. * * @since 1.0.0 * @return self */ public static function instance(): self { if ( null === self::$instance ) { self::$instance = new self(); } return self::$instance; } /** * Prevent cloning to enforce the singleton invariant. * * @since 1.0.0 */ private function __clone() {} /** * Block unserialisation to enforce the singleton invariant. * * @since 1.0.0 * @throws \RuntimeException Always. * @return void */ public function __wakeup(): void { throw new \RuntimeException( 'Cannot unserialise singleton.' ); } /** * Reset the singleton — for use in unit tests only so tests do not leak state. * * @internal * @since 1.0.0 * @return void */ public static function reset_instance(): void { self::$instance = null; } /** * Register all WordPress action hooks required by the plugin. * * @since 1.0.0 * @return void */ private function init_hooks(): void { // Site registration is deliberately NOT hooked here. Registering on // admin_init would transmit the site URL to the proxy service before // the admin has taken any AI action (WP.org Guideline 7 — no phoning // home without consent). ProxyClient and ChatRestController schedule // SiteRegistration::maybe_register() on shutdown of the first // user-initiated proxy request instead. add_action( 'admin_menu', [ $this, 'register_admin_menu' ] ); add_action( 'rest_api_init', [ $this, 'register_rest_routes' ] ); add_action( 'plume_register_menu', [ \Plume\Admin\AdminMenu::class, 'register' ] ); add_action( 'plume_register_rest_routes', [ \Plume\Admin\OnboardingRestController::class, 'register_routes' ] ); add_action( 'plume_register_rest_routes', [ \Plume\Admin\TestKeyRestController::class, 'register_routes' ] ); add_action( 'plume_register_rest_routes', [ \Plume\Admin\ActivationVerifyRestController::class, 'register_routes' ] ); add_action( 'rest_api_init', [ \Plume\Payments\TierUpdateWebhookController::class, 'register' ] ); \Plume\Admin\TierStatusPage::register_hooks(); \Plume\Admin\UsageWidget::register_hooks(); \Plume\Admin\ActivationNotice::register(); \Plume\Admin\TierSyncBackfillNotice::register(); \Plume\Admin\SiteUnreachableNotice::register(); if ( $this->modules->is_enabled( 'chat' ) ) { add_action( 'plugins_loaded', [ \Plume\Modules\Chat\ChatModule::class, 'register' ], 20 ); \Plume\Modules\Editor\EditorModule::register(); } if ( $this->modules->is_enabled( 'generator' ) ) { \Plume\Modules\Generator\GeneratorModule::register(); } if ( $this->modules->is_enabled( 'usage' ) ) { \Plume\Modules\Usage\UsageModule::register(); } // SEO and Images are always registered so their admin pages enqueue assets; // both are available on every tier. \Plume\Modules\Seo\SeoModule::register(); \Plume\Modules\Images\ImagesModule::register(); } /** * Dispatch the admin menu registration action. * * @since 1.0.0 * @return void */ public function register_admin_menu(): void { // Registered fully in Admin\AdminMenu — hooked in P3. do_action( 'plume_register_menu' ); } /** * Dispatch the REST routes registration action. * * @since 1.0.0 * @return void */ public function register_rest_routes(): void { do_action( 'plume_register_rest_routes' ); } // ── Activation / deactivation ───────────────────────────────────────────── /** * Run on plugin activation: create DB tables, seed default options, and schedule cron events. * * @since 1.0.0 * @return void */ public static function activate(): void { Schema::create_tables(); update_option( 'plume_just_activated', true ); // add_option() is a no-op when the option already exists, so an admin who // has explicitly disabled write tools will not have their preference reset. add_option( 'plume_enable_write_tools', true ); self::backfill_site_tier_option(); flush_rewrite_rules(); } /** * One-time migration to seed the site-tier option from a paid user's meta. * * Pre-1.9.0 the paid tier was stored only as per-user meta, which broke * logged-out callers, cron, and CLI. On upgrade we promote that meta value * to the site option so resolution stays correct. * * A `plume_backfill_done` marker is written after the first run so that * repeated activate/deactivate cycles and fresh installs never re-execute * the `get_users()` query. * * @since 1.9.0 * @return void */ private static function backfill_site_tier_option(): void { // Already migrated — skip without touching the DB. if ( get_option( 'plume_backfill_done', false ) ) { return; } if ( false !== get_option( TierManager::SITE_OPTION, false ) ) { update_option( 'plume_backfill_done', true, false ); return; } // One-time migration: 'number' => 1 caps the scan to a single row; the plume_backfill_done // option guard above ensures this never runs again after the first activation. // phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- One-time migration capped at a single row and guarded by plume_backfill_done. $users = get_users( [ 'meta_key' => TierManager::META_KEY, 'meta_value' => [ 'pro_managed', 'pro_byok' ], 'fields' => 'ID', 'number' => 1, ] ); // phpcs:enable WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.SlowDBQuery.slow_db_query_meta_value if ( empty( $users ) ) { update_option( 'plume_backfill_done', true, false ); return; } $tier = (string) get_user_meta( (int) $users[0], TierManager::META_KEY, true ); TierManager::set_site_tier( $tier ); update_option( 'plume_backfill_done', true, false ); } /** * Run on plugin deactivation: shed the orphaned trial-check cron and flush rewrite rules. * * This plugin no longer schedules any cron events — the trial-check cron was * removed along with the trial tier. The defensive clear below stays so that * installs upgraded from a version that did schedule `plume_trial_check` shed * the now-callback-less event on a deactivate/reactivate cycle. * * @since 1.0.0 * @return void */ public static function deactivate(): void { // Defensive: shed the orphaned trial-check event left on installs upgraded // from a version that scheduled it; the callback no longer exists. wp_clear_scheduled_hook( 'plume_trial_check' ); flush_rewrite_rules(); } // ── Accessors ───────────────────────────────────────────────────────────── /** * Return the module registry for this plugin instance. * * @since 1.0.0 * @return ModuleRegistry */ public function modules(): ModuleRegistry { return $this->modules; } }