%s

', esc_html( sprintf( /* translators: %s: comma-separated list of missing requirements. */ __( 'Epic Forms is not running because this site is missing %s.', 'epicforms' ), implode( ', ', $names ) ) ) ); } ); return; } // Require composer autoloader. require_once __DIR__ . '/vendor/autoload.php'; // Table names, shared with the activator and with uninstall.php. require_once __DIR__ . '/includes/class-epicformbuilder-tables.php'; /** * Registers core's own migrations with the runner. * * @since 1.0.0 * * @param array $migrations Registered migrations. * * @return array */ function epicformbuilder_register_core_migrations( array $migrations ): array { $migrations['epicforms'] = [ new Epicformbuilder_Migrations_20260817143000_Hash_Index_Item_Key(), new Epicformbuilder_Migrations_20260817150000_Retire_Db_Version_Option(), new Epicformbuilder_Migrations_20260818073734_Widen_Item_Key(), new Epicformbuilder_Migrations_20260909091500_Add_Anonymized_Utc_Column(), ]; return $migrations; } add_filter( 'epicforms_migrations', 'epicformbuilder_register_core_migrations' ); require_once plugin_dir_path( __FILE__ ) . 'includes/services/class-epicformbuilder-services-submission-token.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/services/class-epicformbuilder-services-pending-values.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/services/class-epicformbuilder-services-submission-merge.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/services/class-epicformbuilder-services-rate-limit.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/migrations/interface-epicformbuilder-migration.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/migrations/interface-epicformbuilder-migration-batched.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/migrations/trait-epicformbuilder-migrations-chores.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/migrations/class-epicformbuilder-migrations-ledger.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/migrations/class-epicformbuilder-migrations-lock.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/migrations/class-epicformbuilder-migrations-result.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/migrations/class-epicformbuilder-migrations-runner.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/migrations/versions/class-epicformbuilder-migrations-20260817143000-hash-index-item-key.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/migrations/versions/class-epicformbuilder-migrations-20260817150000-retire-db-version-option.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/migrations/versions/class-epicformbuilder-migrations-20260818073734-widen-item-key.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/migrations/versions/class-epicformbuilder-migrations-20260909091500-add-anonymized-utc-column.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/class-epicformbuilder-cron.php'; if ( defined( 'WP_CLI' ) && WP_CLI ) { require_once plugin_dir_path( __FILE__ ) . 'includes/migrations/class-epicformbuilder-migrations-cli.php'; WP_CLI::add_command( 'epicforms migrate', 'Epicformbuilder_Migrations_CLI' ); } /** * Current plugin version. */ define( 'EPICFORMS_VERSION', '1.0.1' ); define( 'EPICFORMS_URL', plugin_dir_url( __FILE__ ) ); define( 'EPICFORMS_PATH', plugin_dir_path( __FILE__ ) ); /** * The code that runs during plugin activation. * This action is documented in includes/class-epicformbuilder-activator.php * * @param bool $network_wide Whether the plugin is being activated for every * site in a multisite network. */ function activate_epicformbuilder( $network_wide = false ) { require_once plugin_dir_path( __FILE__ ) . 'includes/class-epicformbuilder-activator.php'; Epicformbuilder_Activator::activate( $network_wide ); } /** * The code that runs during plugin deactivation. * This action is documented in includes/class-epicformbuilder-deactivator.php */ function deactivate_epicformbuilder() { require_once plugin_dir_path( __FILE__ ) . 'includes/class-epicformbuilder-deactivator.php'; Epicformbuilder_Deactivator::deactivate(); } register_activation_hook( __FILE__, 'activate_epicformbuilder' ); register_deactivation_hook( __FILE__, 'deactivate_epicformbuilder' ); /** * Sets up a site added to the network after the plugin was network activated. * * The activation hook fires once, so it can only reach the sites that existed * at that moment. Without this a site created later runs the plugin with no * tables behind it. Priority 20 keeps this behind core's own initializer on * the same hook, which creates the site's tables and then switches back. * * @param WP_Site $new_site The site that was just created. * * @return void */ function epicformbuilder_initialize_new_site( $new_site ) { // wp_initialize_site also fires for front-end signups, WP-CLI and REST, // where wp-admin helpers are not loaded. require_once ABSPATH . 'wp-admin/includes/plugin.php'; // Activated per site rather than network wide? Then the new site gets // nothing until someone activates it there, which runs the normal hook. if ( ! is_plugin_active_for_network( plugin_basename( __FILE__ ) ) ) { return; } switch_to_blog( $new_site->id ); activate_epicformbuilder(); restore_current_blog(); } /** * Adds the plugin's tables to those dropped when a site is deleted. * * Core only drops the tables it owns, so without this the plugin's five tables * survive the site they belonged to and orphan themselves in the database. * * @param string[] $tables Tables queued for deletion. * @param int $site_id The site being deleted. * * @return string[] Tables queued for deletion. */ function epicformbuilder_drop_site_tables( $tables, $site_id ) { global $wpdb; return array_merge( $tables, Epicformbuilder_Tables::all_prefixed( $wpdb->get_blog_prefix( $site_id ) ) ); } if ( is_multisite() ) { add_action( 'wp_initialize_site', 'epicformbuilder_initialize_new_site', 20, 1 ); add_filter( 'wpmu_drop_tables', 'epicformbuilder_drop_site_tables', 10, 2 ); } /** * The core plugin class that is used to define internationalization, * admin-specific hooks, and public-facing site hooks. */ require plugin_dir_path( __FILE__ ) . 'includes/class-epicformbuilder.php'; /** * Begins execution of the plugin. * * Since everything within the plugin is registered via hooks, * then kicking off the plugin from this point in the file does * not affect the page life cycle. * * @since 1.0.0 */ function run_epicformbuilder() { ( new Epicformbuilder() )->run(); } run_epicformbuilder(); add_action( 'plugins_loaded', function () { do_action( 'epicforms_loaded' ); } ); add_action( 'plugins_loaded', [ 'Epicformbuilder_Cron', 'schedule' ] ); add_action( Epicformbuilder_Cron::HOOK, [ 'Epicformbuilder_Cron', 'purge' ] ); // A public form submission must never be the request that pays for a schema // change, and two concurrent anonymous requests must never race one. These // hooks are only where the runner is *offered* work; they are NOT what makes // that true. An earlier version of this comment claimed admin_init was an // authenticated hook — it is not: wp-admin/admin-ajax.php fires admin_init at // line 45 and only checks is_user_logged_in() at line 178, and // wp-admin/admin-post.php fires it at line 27 and checks at line 36, so a // logged-out request to either endpoint reached the runner. Likewise // upgrader_process_complete is reachable from WP-Cron via wp_version_check() // -> wp_maybe_auto_update(), and default WP-Cron is spawned by anonymous // front-end visits. // // The guarantee therefore lives in Runner::may_run(), which refuses any // request that is not WP-CLI, the resume event below, or a logged-in // administrator on a non-ajax request. Read that docblock before adding a // hook here. The cost of the policy is unchanged: a site nobody administers // stays un-migrated, so release code tolerates the pre-migration shape rather // than assuming its own migration has run. add_action( 'upgrader_process_complete', [ 'Epicformbuilder_Migrations_Runner', 'run' ], 10, 0 ); add_action( 'admin_init', [ 'Epicformbuilder_Migrations_Runner', 'run' ] ); // A deliberate, bounded exception to the rule above, and the one case // may_run() lets through unauthenticated: a batched migration that exhausts // its time budget schedules this one-off event to resume it, and default // WP-Cron is triggered by whichever request happens to be due, which can be an // anonymous front-end visit. That is acceptable here in a way it is not for // the rest of run() because step() is time-budgeted and interruptible — the // worst case is one visitor's request spending the budget on background work // — whereas the schema change an unbudgeted migration might run (e.g. ALTER // TABLE) is neither, and blocking that visitor on it is the outcome the // capability check exists to prevent. may_run() recognises this path with // doing_action() on this exact hook, so it stays an exception for this hook // rather than a general "cron may migrate" bypass. Do not remove this or move // it back onto a public hook without re-deciding that trade-off. add_action( 'epicforms_migrations_resume', [ 'Epicformbuilder_Migrations_Runner', 'run' ] ); /** * Warns admins in wp-admin when the last migration run failed. * * A pure reader of Epicformbuilder_Migrations_Runner::FAILURES_OPTION, * which run() itself writes (and clears on a later success). Deliberately * does not call pending() or run() here: this fires during admin_notices, * which renders output, and re-running a migration mid-render would repeat * its side effects and risk corrupting the page if it emits anything or * fatals. Reading the persisted option also means a `wp epicforms migrate` * failure — which never touches wp-admin at all — still shows up here on * the next admin page load. * * @since 1.0.0 * * @return void */ function epicformbuilder_migration_failure_notice(): void { if ( ! current_user_can( 'manage_options' ) ) { return; } $errors = get_option( Epicformbuilder_Migrations_Runner::FAILURES_OPTION ); if ( empty( $errors ) ) { return; } foreach ( (array) $errors as $error ) { printf( '

%s

', esc_html( sprintf( /* translators: 1: contributing plugin slug, 2: migration id, 3: error message. */ __( 'EpicForms could not apply a database update (%1$s / %2$s): %3$s', 'epicforms' ), $error['plugin'], $error['id'], $error['message'] ) ) ); } } add_action( 'admin_notices', 'epicformbuilder_migration_failure_notice' );