HEX
Server: LiteSpeed
System: Linux server315.web-hosting.com 4.18.0-553.54.1.lve.el8.x86_64 #1 SMP Wed Jun 4 13:01:13 UTC 2025 x86_64
User: globfdxw (6114)
PHP: 8.1.34
Disabled: NONE
Upload Files
File: //home/globfdxw/www/wp-content/plugins/wpforms/src/Pro/Tasks/Actions/Migration199Task.php
<?php

namespace WPForms\Pro\Tasks\Actions;

use WPForms\Tasks\Task;
use WPForms\Tasks\Tasks;
use WPForms_Entry_Fields_Handler;
use WPForms_Entry_Handler;

/**
 * Class Migration199Task.
 * Migrate dynamic field values from entry fields to entry_fields table.
 *
 * @since 1.9.9
 */
class Migration199Task extends Task {

	/**
	 * Action name for this task.
	 *
	 * @since 1.9.9
	 */
	public const ACTION = 'wpforms_process_migration_199';

	/**
	 * Status option name.
	 *
	 * @since 1.9.9
	 */
	public const STATUS = 'wpforms_process_migration_199_status';

	/**
	 * Start status.
	 *
	 * @since 1.9.9
	 */
	public const START = 'start';

	/**
	 * In progress status.
	 *
	 * @since 1.9.9
	 */
	public const IN_PROGRESS = 'in progress';

	/**
	 * Completed status.
	 *
	 * @since 1.9.9
	 */
	public const COMPLETED = 'completed';

	/**
	 * Chunk size to use.
	 * Specifies how many entries to process in one db request.
	 *
	 * @since 1.9.9
	 */
	public const CHUNK_SIZE = 1000;

	/**
	 * Chunk size of the migration task.
	 * Specifies how many entry field ids to load at once for further processing.
	 *
	 * @since 1.9.9
	 */
	public const TASK_CHUNK_SIZE = self::CHUNK_SIZE * 10;

	/**
	 * Date from which should get entries to migrate.
	 *
	 * @since 1.9.9
	 */
	public const START_DATE = '2025-11-04';

	/**
	 * Entry handler.
	 *
	 * @since 1.9.9
	 *
	 * @var WPForms_Entry_Handler
	 */
	private $entry_handler;

	/**
	 * Entry fields handler.
	 *
	 * @since 1.9.9
	 *
	 * @var WPForms_Entry_Fields_Handler
	 */
	private $entry_fields_handler;

	/**
	 * Temporary table name.
	 *
	 * @since 1.9.9
	 *
	 * @var string
	 */
	private $temp_table_name;

	/**
	 * Class constructor.
	 *
	 * @since 1.9.9
	 */
	public function __construct() {

		parent::__construct( self::ACTION );
	}

	/**
	 * Initialize the task with all the proper checks.
	 *
	 * @since 1.9.9
	 */
	public function init(): void {

		global $wpdb;

		$this->entry_handler        = wpforms()->obj( 'entry' );
		$this->entry_fields_handler = wpforms()->obj( 'entry_fields' );
		$this->temp_table_name      = "{$wpdb->prefix}wpforms_temp_entries";

		if ( ! $this->entry_handler || ! $this->entry_fields_handler ) {
			return;
		}

		// Bail out if migration is not started or completed.
		$status = get_option( self::STATUS );

		if ( ! $status || $status === self::COMPLETED ) {
			return;
		}

		$this->hooks();

		if ( $status === self::START ) {
			// Mark that migration is in progress.
			update_option( self::STATUS, self::IN_PROGRESS );

			// Init migration.
			$this->init_migration();
		}
	}

	/**
	 * Add hooks.
	 *
	 * @since 1.9.9
	 */
	private function hooks(): void {

		// Register the migrate action.
		add_action( self::ACTION, [ $this, 'migrate' ] );

		// Register after process queue action.
		add_action( 'action_scheduler_after_process_queue', [ $this, 'after_process_queue' ] );
	}

	/**
	 * Migrate entry fields with dynamic values.
	 *
	 * @param int $action_index Action index.
	 *
	 * @since 1.9.9
	 */
	public function migrate( $action_index ): void {

		global $wpdb;

		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching

		// Using OFFSET makes a way longer request, as MySQL has to access all rows before OFFSET.
		// We follow very fast way with indexed column (id > $action_index).
		$entry_records = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT id, entry_id, form_id
				FROM $this->temp_table_name
				WHERE id > %d
				LIMIT %d",
				$action_index,
				self::TASK_CHUNK_SIZE
			)
		);

		if ( empty( $entry_records ) ) {
			return;
		}

		$entry_ids = array_column( $entry_records, 'entry_id' );

		// Get all entries data at once.
		$entries_data = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT entry_id, form_id, fields
				FROM {$this->entry_handler->table_name}
				WHERE entry_id IN (" . implode( ',', $entry_ids ) . ')' // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
			),
			OBJECT_K
		);

		// Process entries in chunks.
		$i             = 0;
		$records_count = count( $entry_records );

		while ( $i < $records_count ) {
			$records_chunk = array_slice( $entry_records, $i, self::CHUNK_SIZE );

			$this->process_entries_dynamic_fields( $records_chunk, $entries_data );

			$i += self::CHUNK_SIZE;
		}

		// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching
	}

	/**
	 * After process queue action.
	 * Set status as completed.
	 *
	 * @since 1.9.9
	 */
	public function after_process_queue(): void {

		$tasks = wpforms()->obj( 'tasks' );

		if ( ! $tasks || $tasks->is_scheduled( self::ACTION ) ) {
			return;
		}

		$this->drop_temp_table();

		// Mark that migration is finished.
		update_option( self::STATUS, self::COMPLETED );
	}

	/**
	 * Init migration.
	 *
	 * @since 1.9.9
	 *
	 * @noinspection PhpUndefinedFunctionInspection
	 */
	private function init_migration(): void {

		// Get all entries that need processing.
		$count = $this->get_entries_for_migration();

		if ( ! $count ) {
			$this->drop_temp_table();

			return;
		}

		$index = 0;

		while ( $index < $count ) {
			// We do not use Task class here as we do not need meta. So, we reduce the number of DB requests.
			as_enqueue_async_action(
				self::ACTION,
				[ $index ],
				Tasks::GROUP
			);

			$index += self::TASK_CHUNK_SIZE;
		}
	}

	/**
	 * Process entries and update dynamic fields in entry_fields table.
	 *
	 * @param array $records      Array of entry records.
	 * @param array $entries_data Array of entry data indexed by entry_id.
	 *
	 * @since 1.9.9
	 */
	private function process_entries_dynamic_fields( $records, $entries_data ): void {

		global $wpdb;

		if ( empty( $records ) || empty( $entries_data ) ) {
			return;
		}

		foreach ( $records as $record ) {
			// Check if we have entry data for this record.
			if ( ! isset( $entries_data[ $record->entry_id ] ) ) {
				continue;
			}

			$entry        = $entries_data[ $record->entry_id ];
			$fields_array = json_decode( $entry->fields, true );

			// Skip if fields is not a valid JSON array.
			if ( ! is_array( $fields_array ) ) {
				continue;
			}

			// Process each field in the entry.
			foreach ( $fields_array as $field ) {
				// Skip if the field is not dynamic.
				if ( ! isset( $field['id'], $field['dynamic'] ) ) {
					continue;
				}

				$field_id      = $field['id'];
				$dynamic_value = $field['value'];

				// Update the entry field with the dynamic value.
				// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
				$result = $wpdb->update(
					$this->entry_fields_handler->table_name,
					[ 'value' => $dynamic_value ],
					[
						'entry_id' => $record->entry_id,
						'form_id'  => $entry->form_id,
						'field_id' => $field_id,
					],
					[ '%s' ],
					[ '%d', '%d', '%d' ]
				);
				// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			}
		}
	}

	/**
	 * Get entries that need migration.
	 * Store them in a temporary table.
	 *
	 * @since 1.9.9
	 *
	 * @return int
	 */
	private function get_entries_for_migration(): int {

		global $wpdb;

		$this->drop_temp_table();

		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$wpdb->query(
			"CREATE TABLE $this->temp_table_name
				(
				    id       BIGINT AUTO_INCREMENT PRIMARY KEY,
				    entry_id BIGINT NOT NULL,
				    form_id  BIGINT NOT NULL,
				    INDEX idx_entry_id (entry_id)
				)"
		);

		// Get entries from the wp_wpforms_entries table after START_DATE.
		$wpdb->query(
			$wpdb->prepare(
				"INSERT INTO $this->temp_table_name (entry_id, form_id)
				SELECT entry_id, form_id
				FROM {$this->entry_handler->table_name}
				WHERE date >= %s",
				self::START_DATE
			)
		);

		// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared

		return $wpdb->rows_affected;
	}

	/**
	 * Drop a temporary table.
	 *
	 * @since 1.9.9
	 */
	private function drop_temp_table(): void {

		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.SchemaChange
		$wpdb->query( "DROP TABLE IF EXISTS $this->temp_table_name" );
	}
}