modules = new Module_Registry(); } /** * The shared instance. * * @return Plugin */ public static function instance(): Plugin { if ( null === self::$instance ) { self::$instance = new self(); } return self::$instance; } /** * Hook everything up. * * @return void */ public function boot(): void { $this->discover_modules(); /** * Fires after auto-discovery so third parties can register their own. * * @param Module_Registry $modules The module registry. */ do_action( 'transtrix_register_modules', $this->modules ); if ( defined( 'WP_CLI' ) && WP_CLI ) { \WP_CLI::add_command( 'transtrix', Cli_Command::class ); } add_action( 'rest_api_init', array( $this, 'register_routes' ) ); add_action( self::CRON_HOOK, array( $this, 'collect_garbage' ) ); if ( ! wp_next_scheduled( self::CRON_HOOK ) ) { wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', self::CRON_HOOK ); } if ( is_admin() ) { ( new Admin_Page() )->register(); } } /** * Daily sweep: stalled runs, old full-site packages, stray package files. * * Stalled runs are also failed whenever History is read, so the list never * shows a run as 'running' after its tab went away. * * @return void */ public function collect_garbage(): void { Job_Store::collect_garbage( array( Tasks::class, 'for_job' ) ); // Staging a restore left behind when its job was failed before anything // could clean it: temp tables and -transtrix-new folders, never the site. $restoring = array_filter( Job_Store::recent( Job_Store::LIMIT ), static function ( $record ) { return 'full-site-restore' === ( $record['task'] ?? '' ) && ! in_array( $record['status'], Job_Store::TERMINAL, true ); } ); if ( ! $restoring ) { ( new Full_Site_Restorer( '' ) )->cleanup( array() ); } Restore_Point::expire( (int) Settings::get( 'restore_retention_days' ) ); Job_Store::prune_full_site( (int) Settings::get( 'full_site_packages' ) ); Package_Store::sweep_parts(); Package_Reader::sweep_stale(); Package_Store::sweep_unused( Job_Store::package_ids() ); } /** * Register the REST surface the admin app consumes. * * The engine's only server contract (invariant A9): every controller is a * thin adapter over an engine call WP-CLI can make too. * * @return void */ public function register_routes(): void { $controllers = array( new Controllers\Modules(), new Controllers\Objects(), new Controllers\Export(), new Controllers\Import(), new Controllers\Jobs(), new Controllers\History(), new Controllers\Settings(), new Controllers\Fullsite(), ); foreach ( $controllers as $controller ) { $controller->register_routes(); } } /** * The module registry. * * @return Module_Registry */ public function modules(): Module_Registry { return $this->modules; } /** * Register every module shipped under includes/Modules. * * A module is any class extending Contracts\Module that lives at * includes/Modules//_Module.php. Dropping a directory in is the * whole installation step; the registry filters out the unavailable ones. * * @return void */ private function discover_modules(): void { $files = glob( TRANSTRIX_PATH . 'includes/Modules/*/*_Module.php' ); foreach ( (array) $files as $file ) { $relative = substr( $file, strlen( TRANSTRIX_PATH . 'includes/' ), -4 ); $class = 'Transtrix\\' . str_replace( '/', '\\', $relative ); if ( ! class_exists( $class ) || ! is_subclass_of( $class, Module::class ) ) { continue; } $this->modules->register( new $class() ); } } }