needs_upgrade() ) { return; } // Only an admin-context request that can manage the site should trigger schema/data // changes. Other admin requests (e.g. admin-ajax from a low-priv user) just skip; // they'll be picked up on the next capable admin's page load. if ( ! current_user_can( 'manage_options' ) ) { return; } // Guard against two overlapping admin requests both running migrations at once. if ( get_transient( self::LOCK_TRANSIENT ) ) { return; } set_transient( self::LOCK_TRANSIENT, 1, MINUTE_IN_SECONDS * 5 ); try { $this->run(); } finally { delete_transient( self::LOCK_TRANSIENT ); } } public function needs_upgrade(): bool { $stored = get_option( self::DB_VERSION_OPTION, '' ); // No stamp yet: either a genuine update from a pre-3.6.5.2 version (including a // client that skipped straight to a later release), or a fresh install. `run()` // checks the selected migrations individually, so both cases are safe. if ( '' === $stored ) { return true; } return version_compare( $stored, JET_FORM_BUILDER_VERSION, '<' ); } /** * Whether the auto-migration set (`migration_instances()`) has not yet fully completed * for this site. On a small site this is true for at most a single `admin_init` request; * on a large site it can span several requests while a time-boxed migration resumes * itself batch by batch. Exposed so UI that depends on the migrated data being complete * (e.g. the "Allowed Server-Side Callbacks" settings tab, whose `import_trusted_callbacks()` * only runs once the whole scan finishes) can disable editing and show a wait state * instead of racing a save against a migration batch still in flight. * * @since 3.6.5.3 */ public function is_migration_in_progress(): bool { return $this->needs_upgrade() && ! $this->all_installed(); } protected function run() { // Nothing outstanding (e.g. fresh install already stamped by table creation) → // just record the version and skip touching the DB in a transaction. if ( $this->all_installed() ) { $this->stamp_version(); return; } try { if ( false === $this->transaction_start() ) { throw new \RuntimeException( 'Failed to start the auto-migration transaction.' ); } $this->install_migrations(); if ( false === $this->transaction_commit() ) { throw new \RuntimeException( 'Failed to commit the auto-migration transaction.' ); } } catch ( \Throwable $exception ) { $this->transaction_rollback(); // Do NOT stamp the version on failure — leave `needs_upgrade()` true so the // next capable admin request retries. Surface the reason for support. if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log '[jet-form-builder] auto-migration failed: ' . $exception->getMessage() ); } return; } // Success → record the version so we don't re-scan on every admin load until the // next update bumps the plugin version again. $this->stamp_version(); } /** * Runs only the migration classes explicitly selected for automatic execution. Each * migration is skipped internally if it is already recorded as installed, so keeping * an old migration in this list is both safe and required for clients that skip the * release where it was introduced. * * A migration signaling an incomplete batch (`Migration_Incomplete_Exception`) stops the * loop immediately instead of continuing to the next migration. Time-boxed migrations * (e.g. `Version_3_6_5_2`, `Version_3_6_5_3`) persist their own progress by issuing an * intermediate `COMMIT` before throwing, so the outer transaction `run()` started is no * longer intact once one of them yields — a later migration run in that same request * would execute outside any transaction, and a genuine failure in it could no longer be * rolled back. Re-throwing right away still leaves `needs_upgrade()` true (via `run()`'s * catch block skipping `stamp_version()`) so the next capable admin request resumes the * interrupted migration, and only then attempts the ones after it. * * @throws \Jet_Form_Builder\Migrations\Migration_Exception * @throws \Jet_Form_Builder\Migrations\Migration_Incomplete_Exception */ protected function install_migrations() { foreach ( $this->migration_instances() as $migration ) { $migration->install(); } } protected function all_installed(): bool { foreach ( $this->migration_instances() as $migration ) { if ( ! $migration->is_installed() ) { return false; } } return true; } /** * Automatic migrations must remain listed in future plugin versions so sites that * jump over the introducing release still receive them. Do not place regular/manual * schema migrations here. * * @return \Jet_Form_Builder\Migrations\Versions\Base_Migration[] */ protected function migration_instances(): array { return array( new Version_3_6_5_2(), new Version_3_6_5_3(), new Version_3_6_6(), ); } protected function stamp_version() { update_option( self::DB_VERSION_OPTION, JET_FORM_BUILDER_VERSION, false ); } protected function transaction_start() { return Execution_Builder::instance()->transaction_start(); } protected function transaction_commit() { return Execution_Builder::instance()->transaction_commit(); } protected function transaction_rollback() { return Execution_Builder::instance()->transaction_rollback(); } }