%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(
'',
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' );