<?php

if ( ! class_exists( 'GFForms' ) ) {
	die();
}

use Gravity_Forms\Gravity_Forms\License;
use Gravity_Forms\Gravity_Forms\Query\Batch_Processing\GF_Entry_Meta_Batch_Processor;
use Gravity_Forms\Gravity_Forms\Query\Batch_Processing\GF_Batch_Operations_Service_Provider;

require_once GF_PLUGIN_DIR_PATH . 'includes/legacy/forms_model_legacy.php';

/**
 * Class GFFormsModel
 *
 * Handles database calls and formatting of stored data regarding forms
 */
class GFFormsModel {

	/**
	 * Stores the values containing and uploaded files for later access
	 *
	 * @since  Unknwon
	 * @access public
	 *
	 * @var array Defaults to an empty array.
	 */
	public static $uploaded_files = array();
	/**
	 * Stores unique form IDs found.
	 *
	 * @since  Unknown
	 * @access public
	 *
	 * @var array Defaults to an empty array.
	 */
	public static $unique_ids = array();

	/**
	 * Stores confirmations found.
	 *
	 * @since  Unknown
	 * @access private
	 *
	 * @var array Defaults to an empty array.
	 */
	private static $_confirmations = array();

	/**
	 * An in-memory cache for the form meta for the current blog.
	 *
	 *  Use "{Blog ID}_{Form ID}" as the key.
	 *
	 * @since   Unknown
	 * @access  private
	 * @example $_current_forms['1_2']
	 *
	 * @var array $_current_forms
	 */
	private static $_current_forms = array();

	/**
	 * An in-memory cache of form properties using "{Blog ID}_{Form ID}" as the key.
	 *
	 * @since 2.7
	 *
	 * @var array $_current_forms_props
	 */
	private static $_current_forms_props = array();

	/**
	 * Handles batch operations for entry meta updates.
	 *
	 * @since 2.5.16
	 *
	 * @var GF_Entry_Meta_Batch_Processor
	 */
	private static $entry_meta_batch_processor;

	/**
	 * The entry data for the current site.
	 *.
	 * @access private
	 *
	 * @var null Defaults to null.
	 */
	private static $_current_lead = null;
	private static $_batch_field_updates = array();
	private static $_batch_field_inserts = array();
	private static $_batch_field_deletes = array();

	/**
	 * Returns the current database version.
	 *
	 * @since 2.2
	 *
	 * @return string
	 */
	public static function get_database_version() {
		static $db_version = array();
		$blog_id = get_current_blog_id();
		if ( empty( $db_version[ $blog_id ] ) ) {
			$db_version[ $blog_id ] = get_option( 'gf_db_version' );
		}

		return $db_version[ $blog_id ];
	}

	/**
	 * Flushes the data stored within GFFormsModel::$_current_forms.
	 *
	 * @since  Unknown
	 * @access public
	 *
	 * @uses GFFormsModel::$_current_forms
	 *
	 * @return void
	 */
	public static function flush_current_forms() {
		self::$_current_forms       = array();
		self::$_current_forms_props = array();
		self::flush_confirmations();
	}

	/**
	 * Flushes the data stored within GFFormsModel::$_current_lead.
	 *
	 * @since  Unknown
	 * @access public
	 *
	 * @uses GFFormsModel::$_current_lead
	 *
	 * @return void
	 */
	public static function flush_current_lead() {
		self::$_current_lead = null;
	}

	/**
	 * Removes the cached properties, meta, and confirmations for a specific form.
	 *
	 * @since 2.6
	 *
	 * @var string $key The cache key.
	 *
	 * @return void
	 */
	public static function flush_current_form( $key ) {
		unset( self::$_current_forms[ $key ], self::$_current_forms_props[ $key ], self::$_confirmations[ $key ] );
	}

	/**
	 * Flushes the data stored within GFFormsModel::$_confirmations
	 *
	 * @since  Unknown
	 * @access public
	 *
	 * @uses GFFormsModel::$_confirmations
	 *
	 * @return void
	 */
	public static function flush_confirmations() {
		self::$_confirmations = array();
	}

	/**
	 * Gets the form table name, including the site's database prefix.
	 *
	 * @since  Unknown
	 * @access public
	 * @global $wpdb
	 *
	 * @return string The form table name.
	 */
	public static function get_form_table_name() {
		global $wpdb;

		if ( version_compare( self::get_database_version(), '2.3-dev-1', '<' ) ) {
			return $wpdb->prefix . 'rg_form';
		}

		return $wpdb->prefix . 'gf_form';
	}

	/**
	 * Gets the form meta table, including the site's database prefix.
	 *
	 * @since  Unknown
	 * @access public
	 * @global $wpdb
	 *
	 * @return string The form meta table.
	 */
	public static function get_meta_table_name() {
		global $wpdb;

		if ( version_compare( self::get_database_version(), '2.3-dev-1', '<' ) ) {
			return $wpdb->prefix . 'rg_form_meta';
		}

		return $wpdb->prefix . 'gf_form_meta';
	}

	/**
	 * Gets the form view table name, including the site's database prefix.
	 *
	 * @since  Unknown
	 * @access public
	 * @global $wpdb
	 *
	 * @return string The form view table name.
	 */
	public static function get_form_view_table_name() {
		global $wpdb;

		if ( version_compare( self::get_database_version(), '2.3-dev-1', '<' ) ) {
			return $wpdb->prefix . 'rg_form_view';
		}

		return $wpdb->prefix . 'gf_form_view';
	}

	/**
	 * Gets the form revisions table name, including the site's database prefix.
	 *
	 * @since  2.4-dev
	 *
	 * @return string The form revisions table name.
	 */
	public static function get_form_revisions_table_name() {
		global $wpdb;

		return $wpdb->prefix . 'gf_form_revisions';
	}

	/**
	 * Gets the lead (entries) table name, including the site's database prefix.
	 *
	 * @since  Unknown
	 * @access public
	 * @global $wpdb
	 *
	 * @return string The lead (entry) table name.
	 */
	public static function get_lead_table_name() {
		return GF_Forms_Model_Legacy::get_lead_table_name();
	}

	/**
	 * Gets the lead (entry) meta table name, including the site's database prefix.
	 *
	 * @since  Unknown
	 * @access public
	 * @global $wpdb
	 *
	 * @return string The lead (entry) meta table name.
	 */
	public static function get_lead_meta_table_name() {
		return GF_Forms_Model_Legacy::get_lead_meta_table_name();
	}

	/**
	 * Gets the lead (entry) notes table name, including the site's database prefix.
	 *
	 * @since  Unknown
	 * @access public
	 * @global $wpdb
	 *
	 * @return string The lead (entry) notes table name.
	 */
	public static function get_lead_notes_table_name() {
		return GF_Forms_Model_Legacy::get_lead_notes_table_name();
	}

	/**
	 * Gets the lead (entry) details table name, including the site's database prefix.
	 *
	 * @since  Unknown
	 * @access public
	 * @global $wpdb
	 *
	 * @return string The lead (entry) details table name.
	 */
	public static function get_lead_details_table_name() {
		return GF_Forms_Model_Legacy::get_lead_details_table_name();
	}

	/**
	 * Gets the lead (entry) details long table name, including the site's database prefix.
	 *
	 * @since  Unknown
	 * @access public
	 * @global $wpdb
	 *
	 * @return string The lead (entry) details long table name.
	 */
	public static function get_lead_details_long_table_name() {
		return GF_Forms_Model_Legacy::get_lead_details_long_table_name();
	}

	/**
	 * Gets the lead (entry) view table name, including the site's database prefix.
	 *
	 * @since  Unknown
	 * @access public
	 * @global $wpdb
	 *
	 * @return string The lead (entry) view table name.
	 */
	public static function get_lead_view_name() {
		return GF_Forms_Model_Legacy::get_lead_view_name();
	}

	/**
	 * Gets the incomplete submissions table name, including the site's database prefix.
	 *
	 * @since  Unknown
	 * @access public
	 * @global $wpdb
	 *
	 * @return string he incomplete submissions table name.
	 */
	public static function get_incomplete_submissions_table_name() {
		return GF_Forms_Model_Legacy::get_incomplete_submissions_table_name();
	}

	/**
	 * Gets the entry table name, including the site's database prefix
	 *
	 * @access public
	 * @static
	 * @global $wpdb
	 *
	 * @return string The entry table name
	 */
	public static function get_entry_table_name() {
		global $wpdb;

		return $wpdb->prefix . 'gf_entry';
	}

	/**
	 * Gets the entry meta table name, including the site's database prefix
	 *
	 * @access public
	 * @static
	 * @global $wpdb
	 *
	 * @return string The entry meta table name
	 */
	public static function get_entry_meta_table_name() {
		global $wpdb;

		return $wpdb->prefix . 'gf_entry_meta';
	}

	/**
	 * Gets the lead (entry) notes table name, including the site's database prefix
	 *
	 * @access public
	 * @static
	 * @global $wpdb
	 *
	 * @return string The lead (entry) notes table name
	 */
	public static function get_entry_notes_table_name() {
		global $wpdb;

		return $wpdb->prefix . 'gf_entry_notes';
	}


	/**
	 * Gets the draft submissions table name, including the site's database prefix
	 *
	 * @access public
	 * @static
	 * @global $wpdb
	 *
	 * @return string The draft submissions table name
	 */
	public static function get_draft_submissions_table_name() {
		global $wpdb;

		return $wpdb->prefix . 'gf_draft_submissions';
	}

	/**
	 * Gets the REST API Key table name, including the site's database prefix
	 *
	 * @access public
	 * @static
	 * @global $wpdb
	 *
	 * @return string The REST API Keys submissions table name
	 */
	public static function get_rest_api_keys_table_name() {
		global $wpdb;

		return $wpdb->prefix . 'gf_rest_api_keys';
	}

	/**
	 * Returns the name of the table where add-on feeds are stored, including the site's database prefix.
	 *
	 * @since 2.4.24
	 *
	 * @return string
	 */
	public static function get_addon_feed_table_name() {
		global $wpdb;

		return $wpdb->prefix . 'gf_addon_feed';
	}


	/**
	 * Gets all forms.
	 *
	 * @since  Unknown
	 * @access public
	 * @global $wpdb
	 *
	 * @uses GFFormsModel::get_form_table_name()
	 * @uses GFFormsModel::get_form_db_columns()
	 * @uses GFFormsModel::get_entry_count_per_form()
	 * @uses GFFormsModel::get_view_count_per_form()
	 *
	 * @param bool   $is_active   Optional. Defines if inactive forms should be displayed. Defaults to null.
	 * @param string $sort_column Optional. The column to be used for sorting the forms. Defaults to 'title'.
	 * @param string $sort_dir    Optional. Defines the direction that sorting should occur. Defaults to 'ASC' (ascending). Use 'DESC' for descending.
	 * @param bool   $is_trash    Optional. Defines if forms within the trash should be displayed. Defaults to false.
	 *
	 * @return array $forms All forms found.
	 */
	public static function get_forms( $is_active = null, $sort_column = 'title', $sort_dir = 'ASC', $is_trash = false ) {
		global $wpdb;
		$form_table_name = esc_sql( self::get_form_table_name() );

		$where_arr   = array();
		$where_arr[] = $wpdb->prepare( 'is_trash=%d', $is_trash );
		if ( $is_active !== null ) {
			$where_arr[] = $wpdb->prepare( 'is_active=%d', $is_active );
		}

		$where_clause = 'WHERE ' . join( ' AND ', $where_arr );
		$sort_keyword = $sort_dir == 'ASC' ? 'ASC' : 'DESC';

		$db_columns = self::get_form_db_columns();

		if ( ! in_array( strtolower( $sort_column ), $db_columns ) ) {
			$sort_column = 'title';
		}

		$sort_column = sanitize_sql_orderby( $sort_column );
		$order_by    = ! empty( $sort_column ) ? "ORDER BY $sort_column $sort_keyword" : '';

		$sql = "SELECT f.id, f.title, f.date_created, f.is_active, 0 as entry_count, 0 view_count
                FROM $form_table_name f
                $where_clause
                $order_by";

		//Getting all forms
		$forms = $wpdb->get_results( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

		//Getting entry count per form
		$entry_count = self::get_entry_count_per_form();

		//Getting view count per form
		$view_count = self::get_view_count_per_form();

		//Adding entry counts and to form array
		foreach ( $forms as &$form ) {
			foreach ( $view_count as $count ) {
				if ( $count->form_id == $form->id ) {
					$form->view_count = $count->view_count;
					break;
				}
			}

			foreach ( $entry_count as $count ) {
				if ( $count->form_id == $form->id ) {
					$form->entry_count = $count->entry_count;
					break;
				}
			}
		}

		return $forms;
	}

	/**
	 * Searches form titles based on query.
	 *
	 * @access public
	 * @static
	 * @global $wpdb
	 * @see GFFormsModel::get_form_table_name
	 * @see GFFormsModel::get_form_db_columns
	 * @see GFFormsModel::get_entry_count_per_form
	 * @see GFFormsModel::get_view_count_per_form
	 *
	 * @param string $query       Optional. The query to search.
	 * @param bool   $is_active   Optional. Defines if inactive forms should be displayed. Defaults to null.
	 * @param string $sort_column Optional. The column to be used for sorting the forms. Defaults to 'title'.
	 * @param string $sort_dir    Optional. Defines the direction that sorting should occur. Defaults to 'ASC' (ascending). Use 'DESC' for descending.
	 * @param bool   $is_trash    Optional. Defines if forms within the trash should be displayed. Defaults to false.
	 *
	 * @return array $forms All forms found.
	 */
	public static function search_forms( $query = '', $is_active = null, $sort_column = 'title', $sort_dir = 'ASC', $is_trash = false ) {
		global $wpdb;
		$form_table_name = esc_sql( self::get_form_table_name() );

		$where_arr   = array();
		$where_arr[] = $wpdb->prepare( 'is_trash=%d', $is_trash );
		if ( $is_active !== null ) {
			$where_arr[] = $wpdb->prepare( 'is_active=%d', $is_active );
		}

		if ( ! rgblank( $query ) ) {
			$where_arr[] = $wpdb->prepare( 'title LIKE %s', '%' . $query . '%' );
		}

		$sort_keyword = $sort_dir == 'ASC' ? 'ASC' : 'DESC';

		$db_columns = self::get_form_db_columns();

		if ( ! in_array( strtolower( $sort_column ), $db_columns ) ) {
			$sort_column = 'title';
		}
		$sort_column = sanitize_sql_orderby( $sort_column );

		$where_clause = 'WHERE ' . join( ' AND ', $where_arr );
		$order_by     = ! empty( $sort_column ) ? "ORDER BY $sort_column $sort_keyword" : '';

		// All of the pieces of this SQL statement have already gone through $wpdb->prepare, so we don't prepare it again here.
		$sql = "SELECT f.id, f.title, f.date_created, f.is_active, 0 as entry_count, 0 view_count
                FROM $form_table_name f
                $where_clause
                $order_by";

		//Getting all forms
		$forms = $wpdb->get_results( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

		//Getting entry count per form
		$entry_count = self::get_entry_count_per_form();

		//Getting view count per form
		$view_count = self::get_view_count_per_form();

		//Adding entry counts and to form array
		foreach ( $forms as &$form ) {
			foreach ( $view_count as $count ) {
				if ( $count->form_id == $form->id ) {
					$form->view_count = $count->view_count;
					break;
				}
			}

			foreach ( $entry_count as $count ) {
				if ( $count->form_id == $form->id ) {
					$form->entry_count = $count->entry_count;
					break;
				}
			}
		}

		return $forms;
	}

	/**
	 * Gets the number of entries per form.
	 *
	 * First attempts to read from cache. If unavailable, gets the entry count, caches it, and returns it.
	 *
	 * @since  Unknown
	 * @access public
	 * @global $wpdb
	 *
	 * @uses GFFormsModel::get_lead_table_name()
	 * @uses GFCache::get()
	 * @uses GFCache::set()
	 *
	 * @return array $entry_count Array of forms, containing the form ID and the entry count
	 */
	public static function get_entry_count_per_form() {

		if ( version_compare( self::get_database_version(), '2.3-dev-1', '<' ) ) {
			return GF_Forms_Model_Legacy::get_entry_count_per_form();
		}

		global $wpdb;
		$entry_table_name = esc_sql( self::get_entry_table_name() );

		$entry_count = GFCache::get( 'get_entry_count_per_form' );
		if ( empty( $entry_count ) ) {
			//Getting entry count per form
			$sql         = $wpdb->prepare( "SELECT form_id, count(id) as entry_count FROM %i l WHERE status=%s GROUP BY form_id", $entry_table_name, 'active' );
			$entry_count = $wpdb->get_results( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

			GFCache::set( 'get_entry_count_per_form', $entry_count, true, 30 );
		}

		return $entry_count;
	}

	/**
	 * Gets the number of views per form
	 *
	 * Checks the cache first.  If not there, gets the count from the database, stores it in the cache, and returns it.
	 *
	 * @since  Unknown
	 * @access public
	 * @global $wpdb
	 *
	 * @uses GFFormsModel::get_form_view_table_name()
	 * @uses GFCache::get()
	 * @uses GFCache::set()
	 *
	 * @return array $view_count Array of forms, containing the form ID and the view count
	 */
	public static function get_view_count_per_form() {
		global $wpdb;
		$view_table_name = esc_sql( self::get_form_view_table_name() );

		$view_count = GFCache::get( 'get_view_count_per_form' );
		if ( empty( $view_count ) ){
			$sql        = "SELECT form_id, sum(count) as view_count FROM $view_table_name GROUP BY form_id";
			$view_count = $wpdb->get_results( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

			GFCache::set( 'get_view_count_per_form', $view_count, true, 30 );
		}

		return $view_count;
	}

	/**
	 * Returns the form database columns.
	 *
	 * @since  Unknown
	 * @access public
	 *
	 * @return array The column IDs
	 */
	public static function get_form_db_columns() {
		return array( 'id', 'title', 'date_created', 'date_updated', 'is_active', 'is_trash' );
	}

	/**
	 * Gets the payment totals for a particular form ID.
	 *
	 * @since  Unknown
	 * @access public
	 * @global $wpdb
	 *
	 * @uses GFFormsModel::get_lead_table_name()
	 *
	 * @param int $form_id The form ID to get payment totals for.
	 *
	 * @return array $totals The payment totals found.
	 */
	public static function get_form_payment_totals( $form_id ) {
		global $wpdb;
		$entry_table_name = version_compare( self::get_database_version(), '2.3-dev-1', '<' ) ? self::get_lead_table_name() : self::get_entry_table_name();

		$sql = $wpdb->prepare(
			"SELECT sum(payment_amount) revenue, count(l.id) orders
             FROM %i l
             WHERE form_id=%d AND payment_amount IS NOT null", $entry_table_name, $form_id
		);

		$totals = $wpdb->get_row( $sql, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

		$active = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			$wpdb->prepare(
				" SELECT count(id) as active
                 FROM %i
                 WHERE form_id=%d AND payment_status='Active'", $entry_table_name, $form_id
			)
		);

		if ( empty( $active ) ) {
			$active = 0;
		}

		$totals['active'] = $active;

		return $totals;
	}

	/**
	 * Gets the total, unread, starred, spam, and trashed entry counts.
	 *
	 * @since  Unknown
	 * @access public
	 * @global $wpdb
	 *
	 * @uses GFFormsModel::get_lead_table_name()
	 * @uses GFFormsModel::get_lead_details_table_name()
	 *
	 * @param int $form_id The ID of the form to check.
	 *
	 * @return array $results[0] The form counts.
	 */
	public static function get_form_counts( $form_id ) {

		if ( version_compare( self::get_database_version(), '2.3-dev-1', '<' ) ) {
			return GF_Forms_Model_Legacy::get_form_counts( $form_id );
		}

		global $wpdb;

		$cache_key = 'form_counts_' . $form_id;

		$results = GFCache::get( $cache_key );

		if ( ! empty( $results ) ) {
			return $results;
		}

		$entry_table_name = self::get_entry_table_name();

		$queries = array(
			"COUNT(DISTINCT CASE WHEN l.status='active' THEN l.id END) as total",
			"COUNT(DISTINCT CASE WHEN l.is_read=0 AND l.status='active' THEN l.id END) as unread",
			"COUNT(DISTINCT CASE WHEN l.is_starred=1 AND l.status='active' THEN l.id END) as starred",
			"COUNT(DISTINCT CASE WHEN l.status='spam' THEN l.id END) as spam",
			"COUNT(DISTINCT CASE WHEN l.status='trash' THEN l.id END) as trash",
		);

		/**
		 * Allows the queries used to get the counts for the entries list filter links to be overridden.
		 *
		 * @since 2.9.16
		 *
		 * @param array $queries The filter count queries.
		 * @param int   $form_id The ID of the form the queries are being prepared for.
		 */
		$filtered_queries = apply_filters( 'gform_entries_filter_count_queries', $queries, $form_id );

		if ( empty( $filtered_queries ) || ! is_array( $filtered_queries ) ) {
			return array();
		}

		if ( $filtered_queries !== $queries ) {
			$filtered_queries = array_filter(
				$filtered_queries,
				function ( $query ) use ( $queries ) {
					if ( in_array( $query, $queries, true ) ) {
						return true;
					}

					$forbidden = '/\b(?:create|drop|alter|delete|truncate|insert|update|replace)\b|;|--|\/\*/i';
					if ( preg_match( $forbidden, $query ) ) {
						return false;
					}

					return true;
				}
			);

			if ( empty( $filtered_queries ) ) {
				$filtered_queries = $queries;
			}
		}

		$queries = implode( ',', $filtered_queries );

		$wpdb->timer_start();
		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$results = $wpdb->get_results( $wpdb->prepare( "SELECT $queries FROM %i l WHERE l.form_id=%d", $entry_table_name, $form_id ), ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		if ( $wpdb->timer_stop() > 1 ) {
			GFCache::set( $cache_key, $results[0], true, 10 * MINUTE_IN_SECONDS );
		}

		return $results[0];
	}

	/**
	 * Gets the form summary for all forms.
	 *
	 * @since  Unknown
	 * @access public
	 * @global $wpdb
	 *
	 * @uses GFFormsModel::get_form_table_name()
	 * @uses GFFormsModel::get_lead_table_name()
	 *
	 * @return array $forms Contains the form summary for all forms.
	 */
	public static function get_form_summary() {
		global $wpdb;
		$form_table_name = esc_sql( self::get_form_table_name() );
		$entry_table_name = version_compare( self::get_database_version(), '2.3-dev-1', '<' ) ? esc_sql( self::get_lead_table_name() ) : esc_sql( self::get_entry_table_name() );

		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$sql = $wpdb->prepare( "SELECT l.form_id, count(l.id) as unread_count
            FROM $entry_table_name l
            WHERE is_read=%d AND status=%s
            GROUP BY form_id",
			0,
			'active'
		);
		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared

		// Getting number of unread and total leads for all forms
		$unread_results = $wpdb->get_results( $sql, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$sql = $wpdb->prepare( "SELECT l.form_id, max(l.date_created) as last_entry_date, count(l.id) as total_entries
            FROM $entry_table_name l
            WHERE status=%s
            GROUP BY form_id",
			'active'
		);
		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared

		$lead_date_results = $wpdb->get_results( $sql, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$sql = $wpdb->prepare( "SELECT id, title, is_trash, '' as last_entry_date, 0 as unread_count
            FROM $form_table_name
            WHERE is_active=%d
            ORDER BY title",
			1
		);
		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared

		$forms = $wpdb->get_results( $sql, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

		for ( $i = 0; $count = sizeof( $forms ), $i < $count; $i ++ ) {
			if ( is_array( $unread_results ) ) {
				foreach ( $unread_results as $unread_result ) {
					if ( $unread_result['form_id'] == $forms[ $i ]['id'] ) {
						$forms[ $i ]['unread_count'] = $unread_result['unread_count'];
						break;
					}
				}
			}

			if ( is_array( $lead_date_results ) ) {
				foreach ( $lead_date_results as $entry_date_result ) {
					if ( $entry_date_result['form_id'] == $forms[ $i ]['id'] ) {
						$forms[ $i ]['last_entry_date'] = $entry_date_result['last_entry_date'];
						$forms[ $i ]['total_entries']    = $entry_date_result['total_entries'];
						break;
					}
				}
			}
		}

		/**
		 * Modifies the summary of all forms, includes unread and total entry counts.
		 *
		 * @since 2.4.16
		 *
		 * @param array $forms Form summary.
		 */
		$forms = apply_filters( 'gform_form_summary', $forms );

		return $forms;
	}

	/**
	 * Gets the total, active, inactive, and trashed form counts.
	 *
	 * @since  Unknown
	 * @access public
	 * @global $wpdb
	 *
	 * @uses GFFormsModel::get_form_table_name()
	 *
	 * @return array The form counts.
	 */
	public static function get_form_count() {
		global $wpdb;
		$form_table_name = self::get_form_table_name();

		if ( ! GFCommon::table_exists( $form_table_name ) ) {
			return array(
				'total'    => 0,
				'active'   => 0,
				'inactive' => 0,
				'trash'    => 0,
			);
		}

		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$results = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			"
            SELECT
            (SELECT count(0) FROM $form_table_name WHERE is_trash = 0) as total,
            (SELECT count(0) FROM $form_table_name WHERE is_active=1 AND is_trash = 0 ) as active,
            (SELECT count(0) FROM $form_table_name WHERE is_active=0 AND is_trash = 0 ) as inactive,
            (SELECT count(0) FROM $form_table_name WHERE is_trash=1) as trash
            "
		);
		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared

		return array(
			'total'    => intval( $results[0]->total ),
			'active'   => intval( $results[0]->active ),
			'inactive' => intval( $results[0]->inactive ),
			'trash'    => intval( $results[0]->trash ),
		);
	}

	/**
	 * Gets the form ID based on the form title.
	 *
	 * @since  Unknown
	 * @access public
	 *
	 * @uses GFFormsModel::get_forms()
	 *
	 * @param string $form_title The form title to search for.
	 *
	 * @return int The form ID. Returns 0 if not found.
	 */
	public static function get_form_id( $form_title ) {
		$forms = self::get_forms();
		foreach ( $forms as $form ) {
			$sanitized_name = str_replace( '[', '', str_replace( ']', '', $form->title ) );
			if ( $form->title == $form_title || $sanitized_name == $form_title ) {
				return absint( $form->id );
			}
		}

		return 0;
	}

	/**
	 * Returns the cache key for the specified form.
	 *
	 * @since 2.7
	 *
	 * @param int $form_id The form ID.
	 *
	 * @return string
	 */
	public static function get_form_cache_key( $form_id ) {
		return get_current_blog_id() . '_' . $form_id;
	}

	/**
	 * Returns an object containing the properties of the specified form or false if the form doesn't exist.
	 *
	 * @since Unknown
	 * @since 2.7 Updated to cache the result.
	 *
	 * @param int  $form_id     The ID of the form to get.
	 * @param bool $allow_trash Optional. Set to true to allow trashed results. Defaults to false.
	 *
	 * @return bool|object
	 */
	public static function get_form( $form_id, $allow_trash = false ) {
		$form_id = absint( $form_id );
		if ( empty( $form_id ) ) {
			return false;
		}

		$key        = self::get_form_cache_key( $form_id );
		$form_props = rgar( self::$_current_forms_props, $key );

		if ( is_object( $form_props ) ) {
			if ( ! $allow_trash && $form_props->is_trash ) {
				return false;
			}

			return $form_props;
		}

		global $wpdb;
		$table_name   = self::get_form_table_name();
		$trash_clause = $allow_trash ? '' : 'AND is_trash = 0';
		$result       = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $table_name WHERE id=%d {$trash_clause}", $form_id ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

		if ( empty( $result ) ) {
			return false;
		}

		self::$_current_forms_props[ $key ] = $result;

		return $result;
	}

	/**
	 * Converts a serialized string or JSON for access in PHP.
	 *
	 * @since  Unknown
	 * @access public
	 *
	 * @param string $string The string to convert.
	 *
	 * @return mixed
	 */
	public static function unserialize( $string ) {
		if ( empty( $string ) ) {
			return null;
		} elseif ( is_serialized( $string ) ) {
			$obj = @unserialize( $string );
		} else {
			$obj = json_decode( $string, true );
		}

		return $obj;
	}

	/**
	 * Gets the form meta based on the form ID.
	 *
	 * @since  Unknown
	 * @access public
	 * @global $wpdb
	 *
	 * @uses GFFormsModel::get_meta_table_name()
	 * @uses GFFormsModel::unserialize()
	 * @uses GFFormsModel::convert_field_objects()
	 * @uses GFFormsModel::load_notifications_from_legacy()
	 * @uses GFFormsModel::$_current_forms
	 *
	 * @param int $form_id The form ID.
	 *
	 * @return array|null $form Form object if found. Null if not found.
	 */
	public static function get_form_meta( $form_id ) {
		global $wpdb;

		$form_id = absint( $form_id );
		if ( empty( $form_id ) ) {
			return null;
		}

		$key = self::get_form_cache_key( $form_id );
		// Return cached version if form meta has been previously retrieved for this form
		if ( isset( self::$_current_forms[ $key ] ) ) {
			return self::$_current_forms[ $key ];
		}

		$table_name = self::get_meta_table_name();
		$form_row   = $wpdb->get_row( $wpdb->prepare( "SELECT display_meta, notifications FROM %i WHERE form_id=%d", $table_name, $form_id ), ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching


		// Loading main form object (supports serialized strings as well as JSON strings)
		$form = self::unserialize( rgar( $form_row, 'display_meta' ) );

		if ( ! $form ) {
			return null;
		}

		// Ensure the fields property is in the correct format, an associative array will cause warnings and js errors in the form editor.
		$form['fields'] = is_array( rgar( $form, 'fields' ) ) ? array_values( $form['fields'] ) : array();

		// Loading notifications
		$form['notifications'] = self::unserialize( $form_row['notifications'] );

		// Creating field objects and copying some form variables down to fields for easier access
		$form = self::convert_field_objects( $form );

		// Loading confirmations from legacy structure into new structure
		$form = self::load_confirmations( $form );

		//only migrate legacy notification if there isn't any notification configured in new structure
		if ( ! isset( $form['notifications'] ) ) {
			$form = self::load_notifications_from_legacy( $form ); // Moving notification data from legacy structure into new 'notifications' array
		}

		// Load notifications to legacy structure to maintain backward compatibility with legacy hooks and functions
		$form = self::load_notifications_to_legacy( $form );

		// Ensure the next field ID is set correctly.
		$form['nextFieldId'] = self::get_next_field_id( $form['fields'] );

		// Ensure the form has a button property.
		if ( rgempty( 'button', $form ) ) {
			$form['button'] = array(
				'type' => 'text',
				'text' => __( 'Submit', 'gravityforms' ),
			);
		}

		/**
		 * Filters the Form object after the form meta is obtained
		 *
		 * @param array $form The Form object
		 */
		$form = gf_apply_filters( array( 'gform_form_post_get_meta', $form_id ), $form );

		// Cached form meta for cheaper retrieval on subsequent requests
		self::$_current_forms[ $key ] = $form;

		return $form;
	}

	/**
	 * Query request checking if the site contains any forms with Legacy markup.
	 *
	 * @since 2.9.1
	 *
	 * @returns bool
	 */
	public static function has_legacy_markup() {
		global $wpdb;

		$table_name             = self::get_meta_table_name();
		$like                   = '%' . $wpdb->esc_like( '"markupVersion":1' ) . '%';
		$count_legacy_markup    = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM %i WHERE display_meta LIKE %s", $table_name, $like ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

		if ( $count_legacy_markup > 0 ) {
			return true;
		}
		return false;
	}


	/**
	 * Recursively checks the highest ID for all the fields in the form and then returns the highest ID + 1.
	 *
	 * @since 2.4.6.12
	 *
	 * @param GF_Field[] $fields
	 * @param int        $next_field_id
	 *
	 * @return int
	 */
	public static function get_next_field_id( $fields, $next_field_id = 1 ) {

		foreach ( $fields as $field ) {

			if ( is_array( $field->fields ) ) {
				$next_field_id = self::get_next_field_id( $field->fields, $next_field_id );
			}

			if ( $field->id >= $next_field_id ) {
				$next_field_id = $field->id + 1;
			}

		}

		return (int) $next_field_id;
	}

	/**
	 * Converts all field objects in a form, based on field type.
	 *
	 * @since  Unknown
	 * @access public
	 *
	 * @uses GF_Field_CreditCard::maybe_upgrade_inputs()
	 *
	 * @param array $form The Form object.
	 *
	 * @return array $form The Form object after the field objects are converted.
	 */
	public static function convert_field_objects( $form ) {
		$page_number    = 1;
		$form['fields'] = is_array( rgar( $form, 'fields' ) ) ? array_values( $form['fields'] ) : array();

		foreach ( $form['fields'] as &$field ) {
			// convert adminOnly property to visibility
			if ( ! isset( $field['visibility'] ) ) {
				$field['visibility'] = isset( $field['adminOnly'] ) && $field['adminOnly'] ? 'administrative' : 'visible';
				unset( $field['adminOnly'] );
			}

			$field = GF_Fields::create( $field );
			if ( isset( $form['id'] ) ) {
				$field->formId = $form['id'];
			}

			$field->pageNumber = $page_number;

			if ( is_array( $field->fields ) ) {
				self::convert_sub_field_objects( $field, $form['id'], $page_number );
			}

			if ( $field->type == 'page' ) {
				$page_number ++;
				$field->pageNumber = $page_number;
			}

			// Populate required cssClass property with empty string if not set to avoid JS errors when rendering.
			if ( ! isset( $field->cssClass ) ) {
				$field->cssClass = '';
			}

			$field->post_convert_field();
		}

		return $form;
	}

	/**
	 * Converts the sub fields to field objects.
	 *
	 * @since 2.4
	 *
	 * @param GF_Field $field         The repeater field to be converted to objects.
	 * @param int      $form_id       The current form ID.
	 * @param int      $page_number   The page number the parent field is located on.
	 * @param int      $nesting_level The level at which a repeater field is nested.
	 */
	private static function convert_sub_field_objects( &$field, $form_id, $page_number, $nesting_level = 0 ) {
		$field->nestingLevel = $nesting_level;
		foreach ( $field->fields as &$field ) {
			$field             = GF_Fields::create( $field );
			$field->formId     = $form_id;
			$field->pageNumber = $page_number;
			$field->post_convert_field();
			if ( is_array( $field['fields'] ) ) {
				$new_nesting_level = $nesting_level + 1;
				self::convert_sub_field_objects( $field, $form_id, $page_number, $new_nesting_level );
			}
		}
	}

	/**
	 * Gets the form meta for multiple forms based on an array for form IDs.
	 *
	 * @since  Unknown
	 * @access public
	 * @global $wpdb
	 *
	 * @uses GFFormsModel::get_form_table_name()
	 * @uses GFFormsModel::get_meta_table_name()
	 * @uses GFFormsModel::unserialize()
	 * @uses GFFormsModel::convert_field_objects()
	 *
	 * @param array $ids Array of form IDs.
	 *
	 * @return array $results
	 */
	public static function get_form_meta_by_id( $ids ) {
		global $wpdb;
		$form_table_name = self::get_form_table_name();
		$meta_table_name = self::get_meta_table_name();

		if ( is_array( $ids ) ) {
			$ids = implode( ',', array_map( 'intval', $ids ) );
		} else {
			$ids = intval( $ids );
		}

		$sql = "SELECT display_meta, confirmations, notifications FROM {$form_table_name} f INNER JOIN {$meta_table_name} m ON f.id = m.form_id WHERE id in({$ids})";
		$results = $wpdb->get_results( $sql, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

		foreach ( $results as &$result ) {
			$form                  = self::unserialize( $result['display_meta'] );
			$form['confirmations'] = self::unserialize( $result['confirmations'] );
			$form['notifications'] = self::unserialize( $result['notifications'] );
			// Creating field objects and copying some form variables down to fields for easier access
			$form   = self::convert_field_objects( $form );
			$result = $form;
		}

		return $results;

	}

	/**
	 * Converts current notification structure to legacy.
	 *
	 * @since  Unknown
	 * @access private
	 *
	 * @param array $form The Form object.
	 *
	 * @return array $form The Form object.
	 */
	private static function load_notifications_to_legacy( $form ) {
		if ( ! is_array( rgar( $form, 'notifications' ) ) ) {
			return $form;
		}

		foreach ( $form['notifications'] as $notification ) {
			if ( ! in_array( rgar( $notification, 'type' ), array( 'user', 'admin' ) ) ) {
				continue;
			}

			$legacy_notification = $notification;

			if ( $notification['toType'] == 'field' ) {
				$legacy_notification['toField'] = $notification['to'];
				unset( $legacy_notification['to'] );
			}

			// Unsetting new properties
			unset( $legacy_notification['toType'] );
			unset( $legacy_notification['id'] );
			unset( $legacy_notification['event'] );
			unset( $legacy_notification['name'] );
			if ( isset( $legacy_notification['type'] ) ) {
				unset( $legacy_notification['type'] );
			}

			//saving into form object
			$property        = $notification['type'] == 'user' ? 'autoResponder' : 'notification';
			$form[ $property ] = $legacy_notification;
		}

		return $form;
	}

	/**
	 * Loads notifications using from the legacy format.
	 *
	 * @since  Unknown
	 * @access private
	 *
	 * @uses GFCommon::has_admin_notification()
	 * @uses GFFormsModel::convert_property_to_merge_tag()
	 * @uses GFCommon::has_user_notification()
	 * @uses GFFormsModel::save_form_notifications()
	 *
	 * @param array $form The Form object.
	 *
	 * @return array $form The Form object.
	 */
	private static function load_notifications_from_legacy( $form ) {

		$form['notifications'] = array();
		if ( GFCommon::has_admin_notification( $form ) ) {
			$admin_notification = $form['notification'];

			//if there is a fromField configured, move it to 'from' as a merge tag
			$admin_notification = self::convert_property_to_merge_tag( $form, $admin_notification, 'from', 'fromField' );

			//if there is a fromNameField configured, move it to 'fromName' as a merge tag
			$admin_notification = self::convert_property_to_merge_tag( $form, $admin_notification, 'fromName', 'fromNameField' );

			//if there is a replyToField configured, move it to 'replyTo' as a merge tag
			$admin_notification = self::convert_property_to_merge_tag( $form, $admin_notification, 'replyTo', 'replyToField' );

			//if routing is configured, set toType to routing, otherwise, set it to email
			$admin_notification['toType'] = ! rgempty( 'routing', $admin_notification ) ? 'routing' : 'email';

			$notification_id = uniqid();

			//assigning this notification to the form_submission action
			$admin_notification['event'] = 'form_submission';
			$admin_notification['name']  = esc_html__( 'Admin Notification', 'gravityforms' );
			$admin_notification['type']  = 'admin';
			$admin_notification['id']    = $notification_id;

			//copying admin notification as an item in the new notifications array
			$form['notifications'][ $notification_id ] = $admin_notification;
		}

		if ( GFCommon::has_user_notification( $form ) ) {

			$user_notification = $form['autoResponder'];

			//if there is a toField configured, set toType to field, if not, set it toemail
			$to_field = rgar( $user_notification, 'toField' );
			if ( ! empty( $to_field ) ) {
				$user_notification['toType'] = 'field';
				$user_notification['to']     = $to_field;
			} else {
				$user_notification['toType'] = 'email';
			}

			$notification_id = uniqid();
			//assigning this notification to the form_submission action
			$user_notification['event'] = 'form_submission';
			$user_notification['name']  = esc_html__( 'User Notification', 'gravityforms' );
			$user_notification['type']  = 'user';
			$user_notification['id']    = $notification_id;

			//copying user notification as an item in the new notifications array
			$form['notifications'][ $notification_id ] = $user_notification;
		}

		self::save_form_notifications( $form['id'], $form['notifications'] );

		return $form;
	}

	/**
	 * Converts a form property to the merge tag format.
	 *
	 * @since  Unknown
	 * @access private
	 *
	 * @uses GFFormsModel::get_field_merge_tag()
	 *
	 * @param array  $form            The Form object.
	 * @param array  $array           Array of properties to search through.
	 * @param string $target_property The property to move the value to.
	 * @param string $source_property The property to search for.
	 *
	 * @return array $array The array that was searched through.
	 */
	private static function convert_property_to_merge_tag( $form, $array, $target_property, $source_property ) {
		$merge_tag = self::get_field_merge_tag( $form, rgar( $array, $source_property ) );
		if ( $merge_tag ) {
			$array[ $target_property ] = $merge_tag;
			unset( $array[ $source_property ] );
		}

		return $array;
	}

	/**
	 * Gets a formatted merge tag for a field.
	 *
	 * @since  Unknown
	 * @access private
	 *
	 * @uses GFFormsModel::get_field()
	 * @uses GFCommon::get_label()
	 *
	 * @param array $form     The Form object.
	 * @param int   $field_id The field ID.
	 *
	 * @return string|false The merge tag if found. False if not found.
	 */
	private static function get_field_merge_tag( $form, $field_id ) {
		$field = self::get_field( $form, $field_id );
		if ( ! $field ) {
			return false;
		}

		return '{' . GFCommon::get_label( $field, $field_id ) . ':' . $field_id . '}';
	}

	/**
	 * Adds default form properties
	 *
	 * @deprecated 1.9
	 * @remove-in 3.0
	 */
	public static function add_default_properties( $form ) {
		_deprecated_function( 'GFFormsModel::add_default_properties', '1.9' );

		if ( is_array( rgar( $form, 'fields' ) ) ) {
			$all_fields = array(
				'adminLabel'        => '', 'allowsPrepopulate' => '', 'defaultValue' => '', 'description' => '', 'content' => '', 'cssClass' => '',
				'errorMessage'      => '', 'id' => '', 'inputName' => '', 'isRequired' => '', 'label' => '', 'noDuplicates' => '',
				'size'              => '', 'type' => '', 'postCustomFieldName' => '', 'displayAllCategories' => '', 'displayCaption' => '', 'displayDescription' => '',
				'displayTitle'      => '', 'displayAlt' => '', 'inputType' => '', 'rangeMin' => '', 'rangeMax' => '', 'calendarIconType' => '',
				'calendarIconUrl'   => '', 'dateType' => '', 'dateFormat' => '', 'phoneFormat' => '', 'addressType' => '', 'defaultCountry' => '', 'defaultProvince' => '',
				'defaultState'      => '', 'hideAddress2' => '', 'hideCountry' => '', 'hideState' => '', 'inputs' => '', 'nameFormat' => '', 'allowedExtensions' => '',
				'captchaType'       => '', 'pageNumber' => '', 'captchaTheme' => '', 'simpleCaptchaSize' => '', 'simpleCaptchaFontColor' => '', 'simpleCaptchaBackgroundColor' => '',
				'failed_validation' => '', 'productField' => '', 'enablePasswordInput' => '', 'maxLength' => '', 'enablePrice' => '', 'basePrice' => '',
				'visibility'        => 'visible',
			);

			foreach ( $form['fields'] as &$field ) {
				if ( is_array( $field ) ) {
					$field = wp_parse_args( $field, $all_fields );
				}
			}
		}

		return $form;
	}

	/**
	 * Gets the column info for the entry listing page.
	 *
	 * @since  Unknown
	 * @access public
	 * @global $wpdb
	 *
	 * @uses GFFormsModel::get_meta_table_name()
	 *
	 * @param int $form_id The ID of the form that entries are coming from.
	 *
	 * @return mixed
	 */
	public static function get_grid_column_meta( $form_id ) {
		global $wpdb;

		$table_name = self::get_meta_table_name();

		return maybe_unserialize( $wpdb->get_var( $wpdb->prepare( "SELECT entries_grid_meta FROM %i WHERE form_id=%d", $table_name, $form_id ) ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
	}

	public static function update_grid_column_meta( $form_id, $columns ) {
		global $wpdb;

		$table_name = self::get_meta_table_name();
		$meta       = maybe_serialize( stripslashes_deep( $columns ) );
		$wpdb->query( $wpdb->prepare( "UPDATE %i SET entries_grid_meta=%s WHERE form_id=%d", $table_name, $meta, $form_id ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
	}

	public static function get_lead_detail_id( $current_fields, $field_number, $item_index = '' ) {
		if ( version_compare( GFFormsModel::get_database_version(), '2.3-dev-1', '<' ) ) {
			return GF_Forms_Model_Legacy::get_lead_detail_id( $current_fields, $field_number );
		}

		foreach ( $current_fields as $field ) {
			if ( (string) $field->meta_key === (string) $field_number && $field->item_index == $item_index) {
				return $field->id;
			}
		}

		return 0;
	}

	/**
	 * Updates the form is_active property in the database.
	 *
	 * @since unknown
	 * @since 2.7 Updated to clear the form from the cache.
	 *
	 * @param int      $form_id   The ID of the form to be updated.
	 * @param int|bool $is_active Indicates if the form is being set as active.
	 *
	 * @return void
	 */
	public static function update_form_active( $form_id, $is_active ) {
		global $wpdb;
		$form_table = self::get_form_table_name();

		$wpdb->query( $wpdb->prepare( "UPDATE %i SET is_active=%d WHERE id=%d", $form_table, $is_active, $form_id ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

		if ( $is_active ) {

			/**
			 * Fires after an inactive form gets marked as active
			 *
			 * @since 1.9
			 *
			 * @param int $form_id The Form ID used to specify which form to activate
			 */
			do_action( 'gform_post_form_activated', $form_id );
		} else {

			/**
			 * Fires after an active form gets marked as inactive
			 *
			 * @since 1.9
			 *
			 * @param int $form_id The Form ID used to specify which form to activate
			 */
			do_action( 'gform_post_form_deactivated', $form_id );
		}

		self::flush_current_form( self::get_form_cache_key( $form_id ) );
	}

	public static function update_notification_active( $form_id, $notification_id, $is_active ) {
		$form = GFFormsModel::get_form_meta( $form_id );

		if ( ! isset( $form['notifications'][ $notification_id ] ) ) {
			return new WP_Error( 'not_found', __( 'Notification not found', 'gravityforms' ) );
		}

		$form['notifications'][ $notification_id ]['isActive'] = (bool) $is_active;

		if ( (bool) $is_active ) {
            /**
             * Fires before a notification is activated
             *
             * @param int   $form['notifications'][ $notification_id ] The ID of the notification that was activated
             * @param array $form                                      The Form object
             */
			do_action( 'gform_pre_notification_activated', $form['notifications'][ $notification_id ], $form );
		} else {
            /**
             * Fires before a notification is deactivated
             *
             * @param int   $form['notifications'][ $notification_id ] The ID of the notification that was deactivated
             * @param array $form                                      The Form object
             */
			do_action( 'gform_pre_notification_deactivated', $form['notifications'][ $notification_id ], $form );
		}

		$result = GFFormsModel::update_form_meta( $form_id, $form['notifications'], 'notifications' );

		return $result;
	}

	public static function update_confirmation_active( $form_id, $confirmation_id, $is_active ) {
		$form = GFFormsModel::get_form_meta( $form_id );

		if ( ! isset( $form['confirmations'][ $confirmation_id ] ) ) {
			return new WP_Error( 'not_found', __( 'Confirmation not found', 'gravityforms' ) );
		}

		$form['confirmations'][ $confirmation_id ]['isActive'] = (bool) $is_active;



		$result = GFFormsModel::update_form_meta( $form_id, $form['confirmations'], 'confirmations' );

		return $result;
	}

	public static function update_forms_active( $forms, $is_active ) {
		foreach ( $forms as $form_id ) {
			self::update_form_active( $form_id, $is_active );
		}
	}

	public static function update_leads_property( $leads, $property_name, $property_value ) {
		self::update_entries_property( $leads, $property_name, $property_value );
	}

	public static function update_lead_property( $lead_id, $property_name, $property_value, $update_akismet = true, $disable_hook = false ) {
		return self::update_entry_property( $lead_id, $property_name, $property_value, $update_akismet, $disable_hook );
	}

	public static function update_entries_property( $leads, $property_name, $property_value ) {
		foreach ( $leads as $lead ) {
			self::update_entry_property( $lead, $property_name, $property_value );
		}
	}

	/**
	 * Changes the status of multiple entries.
	 *
	 * @since 2.9.0
	 * @access public
	 *
	 * @param array  $leads  The entries to transition.
	 * @param string $status The new status.
	 *
	 * @return void
	 */
	public static function change_entries_status( $leads, $status ) {
		foreach ( $leads as $lead ) {
			self::change_entry_status( $lead, $status );
		}
	}

	/**
	 * Changes the status of a single entry.
	 *
	 * @since 2.9.0
	 * @access public
	 *
	 * @param int    $lead_id The entry ID.
	 * @param string $status  The new status.
	 *
	 * @return void
	 */
	public static function change_entry_status( $lead_id, $status ) {
		$lead = self::get_entry( $lead_id );

		// If the entry is already in the desired status, return.
		if ( $lead['status'] == $status ) {
			return;
		}

		// save the current status in the entry meta in case we need to revert
		$previous_status = $lead['status'];
		gform_update_meta( $lead_id, 'previous_status', $previous_status );
		self::update_entry_property( $lead_id, 'status', $status );
	}

	/**
	 * Restores the status of a single entry to its previous status.
	 *
	 * @since 2.9.0
	 * @access public
	 *
	 * @param int $lead_id The entry ID.
	 *
	 * @return void
	 */
	public static function restore_entry_status( $lead_id ) {
		$lead = self::get_entry( $lead_id );

		// If the entry is already active, return.
		if ( $lead['status'] == 'active' ) {
			return;
		}

		// get the previous status from the entry meta
		$previous_status = gform_get_meta( $lead_id, 'previous_status' );
		$new_status = $previous_status === false ? 'active' : $previous_status;
		self::update_entry_property( $lead_id, 'status', $new_status );
	}

	/**
	 * Restores the status of multiple entries to their previous status.
	 *
	 * @since 2.9.0
	 * @access public
	 *
	 * @param array $leads The entries to restore.
	 *
	 * @return void
	 */
	public static function restore_entries_status( $leads ) {
		foreach ( $leads as $lead ) {
			self::restore_entry_status( $lead );
		}
	}

	public static function update_entry_property( $lead_id, $property_name, $property_value, $update_akismet = true, $disable_hook = false ) {
		global $wpdb, $current_user;

		if ( version_compare( GFFormsModel::get_database_version(), '2.3-dev-1', '<' ) ) {
			return GF_Forms_Model_Legacy::update_lead_property( $lead_id, $property_name, $property_value, $update_akismet, $disable_hook );
		}

		$entry_table = self::get_entry_table_name();

		$lead = self::get_entry( $lead_id );

		//marking entry as 'spam' or 'not spam' with Akismet if the plugin is installed
		if ( $update_akismet && GFCommon::akismet_enabled( $lead['form_id'] ) && $property_name == 'status' && in_array( $property_value, array( 'active', 'spam' ) ) ) {

			$current_status = $lead['status'];
			if ( $current_status == 'spam' && $property_value == 'active' ) {
				$form = self::get_form_meta( $lead['form_id'] );
				GFCommon::mark_akismet_spam( $form, $lead, false );
			} else if ( $current_status == 'active' && $property_value == 'spam' ) {
				$form = self::get_form_meta( $lead['form_id'] );
				GFCommon::mark_akismet_spam( $form, $lead, true );
			}
		}

		// If property is trash, log user login
		if ( $property_name == 'status' && $property_value == 'trash' && ! empty( $current_user->user_login ) ) {
			GFCommon::log_debug( __METHOD__ . "(): User ID {$current_user->ID} requested moving of entry #{$lead_id} to trash." );
		}

		//updating lead
		$result = $wpdb->update( $entry_table, array( $property_name => $property_value ), array( 'id' => $lead_id ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

		if ( ! $disable_hook ) {

			$previous_value = rgar( $lead, $property_name );

			if ( $previous_value != $property_value ) {

				// if property is status, prev value is spam and new value is active
				if ( $property_name == 'status' && $previous_value == 'spam' && $property_value == 'active' && ! rgar( $lead, 'post_id' ) ) {
					$lead[ $property_name ] = $property_value;
					$lead['post_id']      = GFCommon::create_post( isset( $form ) ? $form : GFAPI::get_form( $lead['form_id'] ), $lead );
				}

				/**
				 * Fired after an entry property is updated
				 *
				 * @param string $property_name Used within the action string.  Defines the property that fires the action.
				 *
				 * @param int    $lead_id        The Entry ID
				 * @param string $property_value The new value of the property that was updated
				 * @param string $previous_value The previous property value before the update
				 */
				do_action( "gform_update_{$property_name}", $lead_id, $property_value, $previous_value );

				/**
				 * Fired after an entry property is updated.
				 *
				 * @param int    $lead_id        The Entry ID.
				 * @param string $property_name  The property that was updated.
				 * @param string $property_value The new value of the property that was updated.
				 * @param string $previous_value The previous property value before the update.
				 *
				 * @since 2.3.3.9
				 */
				do_action( "gform_post_update_entry_property", $lead_id, $property_name, $property_value, $previous_value );
				gf_feed_processor()->save()->dispatch_on_shutdown();
			}
		}

		return $result;
	}

	private static function truncate( $str, $length ) {
		if ( strlen( $str ) > $length ) {
			$str = substr( $str, 0, $length );
		}

		return $str;
	}

	/**
	 *
	 * @param $leads
	 */
	public static function delete_leads( $leads ) {
		self::delete_entries( $leads );
	}

	public static function delete_entries( $entries ) {
		foreach ( $entries as $entry_id ) {
			self::delete_entry( $entry_id );
		}
	}

	public static function delete_forms( $forms ) {
		foreach ( $forms as $form_id ) {
			self::delete_form( $form_id );
		}
	}

	public static function trash_forms( $form_ids ) {
		foreach ( $form_ids as $form_id ) {
			self::trash_form( $form_id );
		}
	}

	public static function restore_forms( $form_ids ) {
		foreach ( $form_ids as $form_id ) {
			self::restore_form( $form_id );
		}
	}

	public static function delete_leads_by_form( $form_id, $status = '' ) {
		self::delete_entries_by_form( $form_id, $status );
	}

	public static function delete_entries_by_form( $form_id, $status = '' ) {
		global $wpdb, $current_user;

		if ( version_compare( self::get_database_version(), '2.3-dev-1', '<' ) ) {
			GF_Forms_Model_Legacy::delete_leads_by_form( $form_id, $status );
			return;
		}

		$entry_table       = self::get_entry_table_name();
		$entry_notes_table = self::get_entry_notes_table_name();
		$entry_meta_table  = self::get_entry_meta_table_name();

		GFCommon::log_debug( __METHOD__ . "(): Deleting entries for form #{$form_id}." );

		/**
		 * Fires when you delete entries for a specific form
		 *
		 * @param int    $form_id The form ID to specify from which form to delete entries
		 * @param string $status  Allows you to set the form entries to a deleted status
		 */
		do_action( 'gform_delete_entries', $form_id, $status );

		// Get status filter.
		$status_filter = empty( $status ) ? '' : $wpdb->prepare( 'AND status=%s', $status );

		// Get entry IDs.
		$entry_ids = $wpdb->get_col( $wpdb->prepare( "SELECT id FROM $entry_table WHERE form_id=%d {$status_filter}", $form_id ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared

		// If entries were found, loop through them and run action.
		if ( ! empty( $entry_ids ) ) {

		// Log user login for user requesting the deletion of entries
		if ( ! empty( $current_user->user_login ) ) {
			GFCommon::log_debug( __METHOD__ . "(): User ID {$current_user->ID} requested deletion of entries: " . json_encode( $entry_ids ) );
		}

			foreach ( $entry_ids as $entry_id ) {

				/**
				 * Fires before an entry is deleted.
				 *
				 * @param int $entry_id Entry ID to be deleted.
				 */
				do_action( 'gform_delete_entry', $entry_id );


				/**
				 * Fires before a lead is deleted
				 * @param $lead_id
				 * @deprecated Use gform_delete_entry instead
				 * @see gform_delete_entry
				 * @remove-in 3.0
				 */

			if ( has_action( 'gform_delete_lead' ) ) {
				trigger_error( 'The gform_delete_lead action is deprecated and will be removed in 3.0. Use gform_delete_entry instead.', E_USER_DEPRECATED ); // phpcs:ignore QITStandard.PHP.DebugCode.DebugFunctionFound
			}
				do_action( 'gform_delete_lead', $entry_id );

			}

		}

		// Deleting uploaded files
		self::delete_files_by_form( $form_id, $status );
		// Delete from entry notes

		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$sql = $wpdb->prepare(
			"DELETE FROM $entry_notes_table
				WHERE entry_id IN (
					SELECT id FROM $entry_table WHERE form_id=%d {$status_filter}
				)", $form_id
		);
		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

		// Delete from entry meta
		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$sql = $wpdb->prepare(
			"DELETE FROM $entry_meta_table
				WHERE entry_id IN (
					SELECT id FROM $entry_table WHERE form_id=%d {$status_filter}
				)", $form_id
		);
		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

		// Delete from entry
		$sql = $wpdb->prepare( "DELETE FROM $entry_table WHERE form_id=%d {$status_filter}", $form_id ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,
		$wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
	}

	/**
	 * Delete the views for the specified form.
	 *
	 * @param int $form_id The form ID.
	 */
	public static function delete_views( $form_id ) {

		if ( gf_upgrade()->get_submissions_block() ) {
			return new WP_Error( 'submissions_blocked', __( 'Submissions are currently blocked due to an upgrade in progress', 'gravityforms' ) );
		}

		global $wpdb, $current_user;

		// Log user login for user requesting deletion of views
		if ( ! empty( $current_user->user_login ) ) {
			GFCommon::log_debug( __METHOD__ . "(): User ID {$current_user->ID} requested deletion of views for form #{$form_id}." );
		}

		$form_view_table = self::get_form_view_table_name();

		//Delete form view
		$sql = $wpdb->prepare( "DELETE FROM %i WHERE form_id=%d", $form_view_table, $form_id );
		$wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

		/**
         * Fires after form views are deleted
         *
         * @param int $form_id The ID of the form that views were deleted from
         */
		do_action( 'gform_post_form_views_deleted', $form_id );
	}

	public static function delete_form( $form_id ) {

		if ( gf_upgrade()->get_submissions_block() ) {
			return new WP_Error( 'submissions_blocked', __( 'Submissions are currently blocked due to an upgrade in progress', 'gravityforms' ) );
		}

		global $wpdb, $current_user;

		// Log user login for user requesting deletion of form
		if ( ! empty( $current_user->user_login ) ) {
			GFCommon::log_debug( __METHOD__ . "(): User ID {$current_user->ID} requested deletion of form #{$form_id}." );
		}

        /**
         * Fires before a form is deleted
         *
         * @param int $form_id The ID of the form being deleted
         */
		do_action( 'gform_before_delete_form', $form_id );

		$form_meta_table      = self::get_meta_table_name();
		$form_table           = self::get_form_table_name();
		$form_revisions_table = self::get_form_revisions_table_name();

		//Deleting form Entries
		self::delete_leads_by_form( $form_id );

		//Delete form meta
		$sql = $wpdb->prepare( "DELETE FROM %i WHERE form_id=%d", $form_meta_table, $form_id );
		$wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

		//Delete form revisions
		$sql = $wpdb->prepare( "DELETE FROM %i WHERE form_id=%d", $form_revisions_table, $form_id );
		$wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

		//Deleting form Views
		self::delete_views( $form_id );

		//Delete form
		$sql = $wpdb->prepare( "DELETE FROM %i WHERE id=%d", $form_table, $form_id );
		$wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

		self::flush_current_form( self::get_form_cache_key( $form_id ) );

        /**
         * Fires after a form is deleted
         *
         * @param int $form_id The ID of the form that was deleted
         */
		do_action( 'gform_after_delete_form', $form_id );
	}

	public static function trash_form( $form_id ) {

		if ( gf_upgrade()->get_submissions_block() ) {
			return new WP_Error( 'submissions_blocked', __( 'Submissions are currently blocked due to an upgrade in progress', 'gravityforms' ) );
		}

		global $wpdb, $current_user;
		// Log user login for user moving the form to trash
		if ( ! empty( $current_user->user_login ) ) {
			GFCommon::log_debug( __METHOD__ . "(): User ID {$current_user->ID} requested moving of form #{$form_id} to trash." );
		}
		$form_table_name = self::get_form_table_name();
		$sql             = $wpdb->prepare( "UPDATE %i SET is_trash=1 WHERE id=%d", $form_table_name, $form_id );
		$result          = $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

		self::flush_current_form( self::get_form_cache_key( $form_id ) );

		$success = $result == false;

		/**
		 * Fires after a form is trashed
		 *
		 * @since 1.9
		 *
		 * @param int $form_id The ID of the form that was trashed
		 */
		do_action( 'gform_post_form_trashed', $form_id );

		self::update_recent_forms( $form_id, true );

		return $success;
	}

	public static function restore_form( $form_id ) {

		if ( gf_upgrade()->get_submissions_block() ) {
			return new WP_Error( 'submissions_blocked', __( 'Submissions are currently blocked due to an upgrade in progress', 'gravityforms' ) );
		}

		global $wpdb;
		$form_table_name = self::get_form_table_name();
		$sql             = $wpdb->prepare( "UPDATE %i SET is_trash=0 WHERE id=%d", $form_table_name, $form_id );
		$result          = $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

		self::flush_current_form( self::get_form_cache_key( $form_id ) );

		$success = $result == false;

        /**
         * Fires after a form is restored from trash
         *
         * @since 1.9
         *
         * @param int $form_id The ID of the form that was restored
         */
		do_action( 'gform_post_form_restored', $form_id );

		return $success;
	}

	/**
	 * Duplicate form.
	 *
	 * @access public
	 * @static
	 * @param  int $form_id Form ID to duplicate.
	 *
	 * @return int|WP_Error
	 */
	public static function duplicate_form( $form_id ) {

		if ( gf_upgrade()->get_submissions_block() ) {
			return new WP_Error( 'submissions_blocked', __( 'Submissions are currently blocked due to an upgrade in progress', 'gravityforms' ) );
		}

		// Get form to be duplicated.
		$form = self::get_form( $form_id );

		// Set initial form title.
		$new_title = self::maybe_increment_title( $form->title );

		// Create new form.
		$new_id = self::insert_form( $new_title );

		// Copying form meta to new form.
		$meta          = self::get_form_meta( $form_id );
		$meta['title'] = $new_title;
		$meta['id']    = $new_id;

		// Add notifications to new form.
		self::update_form_meta( $new_id, $meta['notifications'], 'notifications' );
		unset( $meta['notifications'] );

		// Add confirmations to new form.
		self::update_form_meta( $new_id, $meta['confirmations'], 'confirmations' );
		unset( $meta['confirmations'] );

		// Save form meta.
		self::update_form_meta( $new_id, $meta );

		// Set active state.
		self::update_form_active( $new_id, $form->is_active );

		// The gform_after_duplicate_form action is deprecated since version 1.9. Please use gform_post_form_duplicated instead

        /**
         * @deprecated
         * @see gform_post_form_duplicated
         * @remove-in 3.0
         */

		if ( has_action( 'gform_after_duplicate_form' ) ) {
			trigger_error( 'The gform_after_duplicate_form action is deprecated and will be removed in 3.0. Use gform_post_form_duplicated instead.', E_USER_DEPRECATED ); // phpcs:ignore QITStandard.PHP.DebugCode.DebugFunctionFound
		}
        do_action( 'gform_after_duplicate_form', $form_id, $new_id );


		/**
		 * Fires after a form is duplicated
		 *
		 * @param int $form_id The original form's ID
		 * @param int $new_id  The ID of the new, duplicated form
		 */
		do_action( 'gform_post_form_duplicated', $form_id, $new_id );

		return $new_id;

	}


	public static function is_unique_title( $title, $form_id=0 ) {
		$forms = self::get_forms();
		foreach ( $forms as $form ) {
			if ( strtolower( $form->title ) === strtolower( $title ) && (int) $form->id !== (int) $form_id ) {
				return false;
			}
		}

		return true;
	}

	/**
	 * If a form title isn't unique, add a number to it to make it unique.
	 *
	 * @since 2.5
	 *
	 * @param string     $title
	 * @param int|string $form_id
	 *
	 * @return string
	 */
	public static function maybe_increment_title( $title, $form_id = '' ) {
		// Title is unique, so we don't need to do anything to it.
		if ( self::is_unique_title( $title, $form_id ) ) {
			return $title;
		}

		// Check for form count in form title.
		preg_match_all( '/(\\(([0-9])*\\))$/mi', $title, $count_exists_in_title );

		// If count does not exist, set count to 1.
		if ( empty( $count_exists_in_title[0] ) ) {

			// Set initial count.
			$count = 1;

		} else {

			// Set existing count to current count plus one.
			$count = (int) $count_exists_in_title[2][0] + 1;

			// Remove existing count from title.
			$title = preg_replace( '/(\\(([0-9])*\\))$/mi', '', $title );

		}

		// Trim title.
		$title = trim( $title );

		// Add copy count to form title.
		$new_title = $title . " ($count)";

		// If new form title is not unique, increment the count until a unique form title is created.
		while ( ! self::is_unique_title( $new_title, $form_id ) ) {
			$count++;
			$new_title = $title . " ($count)";
		}

		return $new_title;
	}

	public static function ensure_tables_exist() {
		global $wpdb;
		$form_table_name = self::get_form_table_name();
		$form_count      = $wpdb->get_var( $wpdb->prepare( "SELECT count(0) FROM %i", $form_table_name ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		if ( $wpdb->last_error ) {
			GFCommon::log_debug( 'GFFormsModel::ensure_tables_exist(): Blog ' . get_current_blog_id() . ' - Form database table does not exist. Forcing database setup.' );
			gf_upgrade()->upgrade_schema();
		}
	}

	public static function insert_form( $form_title ) {
		global $wpdb;
		$form_table_name = self::get_form_table_name();

		//creating new form
		$wpdb->query( $wpdb->prepare( "INSERT INTO %i(title, date_created) VALUES(%s, utc_timestamp())", $form_table_name, $form_title ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

		//returning newly created form id
		return $wpdb->insert_id;

	}

	/**
	 * Update form meta.
	 *
	 * @since 2.4 Added the form revision creation functionality.
	 *
	 * @param int    $form_id Form id.
	 * @param array  $form_meta Form meta.
	 * @param string $meta_name Meta name.
	 *
	 * @return false|int Number of rows affected/selected or false on error.
	 */
	public static function update_form_meta( $form_id, $form_meta, $meta_name = 'display_meta' ) {
		global $wpdb;

		$form_meta = gf_apply_filters( array( 'gform_form_update_meta', $form_id ), $form_meta, $form_id, $meta_name );

		$meta_table_name = self::get_meta_table_name();
		$new_display_meta = $form_meta;
		$form_meta       = json_encode( $form_meta );

		if ( $meta_name === 'display_meta' ) {
			self::maybe_create_form_revision( $new_display_meta, $form_id );
		}

		if ( intval( $wpdb->get_var( $wpdb->prepare( "SELECT count(0) FROM %i WHERE form_id=%d", $meta_table_name, $form_id ) ) ) > 0 ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			$result = $wpdb->query( $wpdb->prepare( "UPDATE %i SET %i=%s WHERE form_id=%d", $meta_table_name, $meta_name, $form_meta, $form_id ) );
		} else {
			$result = $wpdb->query( $wpdb->prepare( "INSERT INTO %i(form_id, %i) VALUES(%d, %s)", $meta_table_name, $meta_name, $form_id, $form_meta ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		}

		self::flush_current_form( self::get_form_cache_key( $form_id ) );

		/**
		 * Fires after form meta has been updated for any form
		 *
		 * @param mixed  $form_meta The Form Meta object from the database
		 * @param int    $form_id   The ID of the form data was updated
		 * @param string $meta_name The name of the meta updated
		 */
		gf_do_action( array( 'gform_post_update_form_meta', $form_id ), $form_meta, $form_id, $meta_name );

		return $result;
	}

	/**
	 * Create form revision if conditions met.
	 *
	 * @since 2.4
	 *
	 * @param array $new_display_meta Form meta.
	 * @param int    $form_id Form ID.
	 */
	public static function maybe_create_form_revision( $new_display_meta, $form_id ) {
		global $wpdb;

		// Make sure the form isn't in the cache before calling get_form_meta().
		self::flush_current_form( self::get_form_cache_key( $form_id ) );
		$form = self::get_form_meta( $form_id );
		// check if form has consent field.
		if ( GFCommon::has_consent_field( $new_display_meta ) ) {
			$revisions_table_name = self::get_form_revisions_table_name();

			// create the first revision.
			if ( intval( $wpdb->get_var( $wpdb->prepare( "SELECT count(0) FROM %i WHERE form_id=%d", $revisions_table_name, $form_id ) ) ) === 0 ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
				$wpdb->query( $wpdb->prepare( "INSERT INTO %i(form_id, display_meta, date_created) VALUES (%d, %s, utc_timestamp())", $revisions_table_name, $form_id, json_encode( $new_display_meta ) ) );

				return;
			}

			$old_consent_fields = array();
			foreach ( $form['fields'] as $field ) {
				if ( $field->get_input_type() === 'consent' ) {
					$old_consent_fields[ $field->id ] = $field->description;
				}
			}

			// check if consent field description changed.
			$create_revision = false;
			foreach ( $new_display_meta['fields'] as $field ) {
				if ( self::get_input_type( $field ) === 'consent' ) {
					if ( $field['description'] !== rgar( $old_consent_fields, $field['id'] ) ) {
						$create_revision = true;

						break;
					}
				}
			}

			if ( $create_revision ) {
				$wpdb->query( $wpdb->prepare( "INSERT INTO %i (form_id, display_meta, date_created) VALUES(%d, %s, utc_timestamp())", $revisions_table_name, $form_id, json_encode( $new_display_meta ) ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			}
		}
	}

	/**
	 * Get the latest revision ID from form revisions.
	 *
	 * @since 2.4
	 *
	 * @param int $form_id Form ID.
	 *
	 * @return int Revision ID.
	 */
	public static function get_latest_form_revisions_id( $form_id ) {
		global $wpdb;
		$revisions_table_name = GFFormsModel::get_form_revisions_table_name();
		$value                = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM %i WHERE form_id=%d ORDER BY date_created DESC, id DESC LIMIT 1", $revisions_table_name, $form_id ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

		return $value;
	}

	public static function delete_files( $lead_id, $form = null ) {
		$lead = self::get_lead( $lead_id );

		if ( $form == null ) {
			$form = self::get_form_meta( $lead['form_id'] );
		}

		$field_types = self::get_delete_file_field_types( $form );
		$fields      = self::get_fields_by_type( $form, $field_types );

		if ( is_array( $fields ) ) {
			foreach ( $fields as $field ) {
				if ( $field instanceof GF_Field_FileUpload ) {
					$files = $field->to_array( self::get_lead_field_value( $lead, $field ) );
					foreach ( $files as $file ) {
						self::delete_physical_file( $file, $lead_id );
					}
				} elseif ( $field->multipleFiles ) {
					$value_json = self::get_lead_field_value( $lead, $field );
					if ( ! empty( $value_json ) ) {
						$files = json_decode( $value_json, true );
						if ( false === empty( $files ) && is_array( $files ) ) {
							foreach ( $files as $file ) {
								self::delete_physical_file( $file, $lead_id );
							}
						}
					}
				} else {
					$value = self::get_lead_field_value( $lead, $field );
					self::delete_physical_file( $value, $lead_id );
				}
			}
		}
	}

	public static function delete_files_by_form( $form_id, $status = '' ) {
		global $wpdb;

		$entry_table_name = version_compare( self::get_database_version(), '2.3-dev-1', '<' ) ? self::get_lead_table_name() : self::get_entry_table_name();

		$form        = self::get_form_meta( $form_id );
		$field_types = self::get_delete_file_field_types( $form );
		$fields      = self::get_fields_by_type( $form, $field_types );

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

		$status_filter = empty( $status ) ? '' : $wpdb->prepare( 'AND status=%s', $status );
		$results       = $wpdb->get_results( $wpdb->prepare( "SELECT id FROM %i WHERE form_id=%d {$status_filter}", $entry_table_name, $form_id ), ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared

		foreach ( $results as $result ) {
			self::delete_files( $result['id'], $form );
		}
	}

	/**
	 * Returns an array of field types for which can uploaded files can be deleted.
	 *
	 * @since 2.4.6.1
	 *
	 * @param array $form The current form.
	 *
	 * @return array
	 */
	public static function get_delete_file_field_types( $form ) {
		$field_types = array( 'fileupload', 'post_image' );

		/**
		 * Allows more files to be deleted
		 *
		 * @since 1.9.10
		 *
		 * @param array $field_types Field types which contain file uploads
		 * @param array $form The Form Object
		 */
		return gf_apply_filters( array( 'gform_field_types_delete_files', $form['id'] ), $field_types, $form );
	}

	/**
	 * Deletes the uploaded files for the specified form and field.
	 *
	 * Note: Does not delete the file URLs from the entries, that is done by GFFormsModel::delete_field_values().
	 *
	 * @since 2.4.6.1
	 *
	 * @since 2.5.16 changed the query to return entry ID as well.
	 *
	 * @param int $form_id The current form ID.
	 * @param int $field_id The ID of field being deleted.
	 */
	public static function delete_field_files( $form_id, $field_id ) {
		if ( version_compare( self::get_database_version(), '2.3-dev-1', '<' ) ) {
			return;
		}

		global $wpdb;

		$entry_meta_table_name = self::get_entry_meta_table_name();

		$results = $wpdb->get_results( $wpdb->prepare( "SELECT entry_id, meta_value FROM %i WHERE form_id=%d AND meta_key=%s", $entry_meta_table_name, $form_id, $field_id ), ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		if ( is_array( $results ) ) {
			foreach ( $results as $result ) {
				if ( ! is_array( $result ) || empty( rgar( $result, 'meta_value' ) ) ) {
					continue;
				}
				if ( strpos( $result['meta_value'], '[' ) === 0 ) {
					// Value from a multi-file enabled field.
					$files = json_decode( $result['meta_value'], true );

					if ( is_array( $files ) ) {
						foreach ( $files as $file ) {
							self::delete_physical_file( $file, $result['entry_id'] );
						}
					}
				} else {
					// Value from a single file or post image field.
					self::delete_physical_file( $result['meta_value'], $result['entry_id'] );
				}
			}
		}
	}

	public static function delete_file( $entry_id, $field_id, $file_index = 0 ) {
		global $wpdb;

		if ( version_compare( self::get_database_version(), '2.3-dev-1', '<' ) ) {
			GF_Forms_Model_Legacy::delete_file( $entry_id, $field_id, $file_index );
			return;
		}


		if ( $entry_id == 0 || $field_id == 0 ) {
			return;
		}

		$entry = self::get_lead( $entry_id );
		if ( empty( $entry ) ) {
			return;
		}

		$entry_value = rgar( $entry, $field_id );
		if ( empty( $entry_value ) ) {
			return;
		}

		$form_id        = $entry['form_id'];
		$form           = self::get_form_meta( $form_id );
		$field          = self::get_field( $form, $field_id );
		$multiple_files = $field->multipleFiles;

		if ( $field instanceof GF_Field_FileUpload ) {
			$files    = $field->to_array( $entry_value );
			$file_url = rgar( $files, $file_index );
			unset( $files[ $file_index ] );
			$field_value = $field->to_string( $files );
		} elseif ( $multiple_files ) {
			$file_urls = json_decode( $entry_value, true );
			$file_url  = $file_urls[ $file_index ];
			unset( $file_urls[ $file_index ] );
			$file_urls   = array_values( $file_urls );
			$field_value = empty( $file_urls ) ? '' : json_encode( $file_urls );
		} else {
			$file_url    = $entry_value;
			$field_value = '';
		}

		self::delete_physical_file( $file_url, $entry_id );

		// Update entry field value - simulate form submission.
		$entry_meta_table_name = self::get_entry_meta_table_name();
		$sql                   = $wpdb->prepare( "SELECT id FROM %i WHERE entry_id=%d AND meta_key = %s", $entry_meta_table_name, $entry_id, $field_id );
		$entry_meta_id         = $wpdb->get_var( $sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared

		self::update_entry_field_value( $form, $entry, $field, $entry_meta_id, $field_id, $field_value );

	}

	private static function delete_physical_file( $file_url, $entry_id ) {

		$ary = explode( '|:|', $file_url );
		$url = rgar( $ary, 0 );
		if ( empty( $url ) ) {
			return;
		}

		$file_path = self::get_physical_file_path( $url, $entry_id );

		/**
		 * Allow the file path to be overridden so files stored outside the /wp-content/uploads/gravity_forms/ directory can be deleted.
		 *
		 * @since 2.2.3.1
		 *
		 * @param string $file_path The path of the file to be deleted.
		 * @param string $url       The URL of the file to be deleted.
		 */
		$file_path = apply_filters( 'gform_file_path_pre_delete_file', $file_path, $url );

		// If the file URL is outside the uploads folder, something nefarious is up, so bail.
		if ( ! GFCommon::is_file_in_uploads( $url ) ) {
			GFCommon::log_debug( __METHOD__ . sprintf( '(): Not deleting file from URL: %s', $file_path ) );
			return;
		}

		// Verify the resolved file path is within the uploads root (defense-in-depth against filter manipulation).
		$resolved_path = GFCommon::get_absolute_path( $file_path );
		$upload_root   = trailingslashit( GFCommon::get_absolute_path( self::get_upload_root() ) );
		if ( ! str_starts_with( $resolved_path, $upload_root ) ) {
			GFCommon::log_debug( __METHOD__ . sprintf( '(): Not deleting file; resolved path is outside the uploads root: %s', $file_path ) );
			return;
		}

		// if file path contains traversal characters or null bytes, something nefarious is up, so bail.
		$path_validation = GF_Download::validate_file_path( $file_path );
		if ( is_wp_error( $path_validation ) ) {
			GFCommon::log_debug( __METHOD__ . sprintf( '(): Not deleting file (%s): %s', $path_validation->get_error_code(), $file_path ) );
			return;
		}

		if ( file_exists( $file_path ) ) {
			$result = unlink( $file_path );

			if ( $result ) {
				GFCommon::log_debug( __METHOD__ . "(): File {$file_path} for entry #{$entry_id} was successfully deleted." );
			} else {
				GFCommon::log_debug( __METHOD__ . "(): File {$file_path} for entry #{$entry_id} could not be deleted even though it exists. This is likely due to server permissions, ownership, or another file system error." );
			}

			gform_delete_meta( $entry_id, GF_Field_FileUpload::get_file_upload_path_meta_key_hash( $url ) );
		} else {
			GFCommon::log_debug( __METHOD__ . "(): File {$file_path} for entry #{$entry_id} is inaccessible, likely because it no longer exists or due to issues with server permissions or ownership, or another file system error." );
		}
	}

	/**
	 * Gets the physical path of a file.
	 *
	 * @since unknown
	 *
	 * @since 2.5.16  Added optional $entry_id parameter.
	 *
	 * @param string       $url
	 * @param integer|null $entry_id
	 *
	 * @return string
	 */
	public static function get_physical_file_path( $url, $entry_id = null ) {

		$path_info = GF_Field_FileUpload::get_file_upload_path_info( $url, $entry_id );

		// convert from url to physical path
		if ( is_multisite() && get_site_option( 'ms_files_rewriting' ) ) {
			$file_path = preg_replace( "|^(.*?)/files/gravity_forms/|", BLOGUPLOADDIR . 'gravity_forms/', $url );
		} else {
			$file_path = str_replace( trailingslashit( $path_info['url'] ), trailingslashit( $path_info['path'] ), $url );
		}

		return $file_path;
	}

	public static function delete_field( $form_or_id, $field_id, $save_form = true ) {
		$form = is_numeric( $form_or_id ) ? self::get_form_meta( $form_or_id ) : $form_or_id;

		if ( empty( $form['id'] ) || ! isset( $form['fields'] ) || ! is_array( $form['fields'] ) ) {
			return null;
		}

		$form_id = $form['id'];

        /**
         * Fires before a field is deleted
         *
         * @param int $form_id  The ID of the form that the field is being deleted from
         * @param int $field_id The ID of the field being deleted
         */
		do_action( 'gform_before_delete_field', $form_id, $field_id );

		$field_type = '';

		$count = sizeof( $form['fields'] );
		for ( $i = $count - 1; $i >= 0; $i -- ) {
			/** @var GF_Field $field */
			$field = $form['fields'][ $i ];

			// Deleting associated conditional logic rules.
			if ( ! empty( $field->conditionalLogic ) ) {
				$field->conditionalLogic = self::delete_field_from_conditional_logic( $field->conditionalLogic, $field_id );
			}

			if ( $field->type === 'page' && ! empty( $field->nextButton['conditionalLogic'] ) ) {
				$field->nextButton['conditionalLogic'] = self::delete_field_from_conditional_logic( $field->nextButton['conditionalLogic'], $field_id );
			}

			// Deleting field from form meta.
			if ( $field->id == $field_id ) {
				$field_type = $field->type;
				unset( $form['fields'][ $i ] );
			}
		}

		// The field has already been removed from the form passed by GFFormDetail::save_form_info(), get the field from the db.
		if ( empty( $field_type ) && $deleted_field = self::get_field( $form_id, $field_id ) ) {
			$field_type = $deleted_field->type;
		}

		// Removing post content and title template if the field being deleted is a post content field or post title field.
		if ( $field_type == 'post_content' ) {
			$form['postContentTemplateEnabled'] = false;
			$form['postContentTemplate']        = '';
		} else if ( $field_type == 'post_title' ) {
			$form['postTitleTemplateEnabled'] = false;
			$form['postTitleTemplate']        = '';
		}

		if ( ! empty( $form['button']['conditionalLogic'] ) ) {
			$form['button']['conditionalLogic'] = self::delete_field_from_conditional_logic( $form['button']['conditionalLogic'], $field_id );
		}

		// Notifications/confirmations are not present in the form passed by GFFormDetail::save_form_info() but they could be present in other scenarios.
		$form = GFFormsModel::delete_field_from_confirmations( $form, $field_id );
		$form = GFFormsModel::delete_field_from_notifications( $form, $field_id );

		if ( in_array( $field_type, self::get_delete_file_field_types( $form ) ) ) {
			self::delete_field_files( $form_id, $field_id );
		}

		$form['fields'] = array_values( $form['fields'] );

		if ( $save_form ) {
			self::update_form_meta( $form_id, $form );
		}

		//Delete from grid column meta
		$columns = self::get_grid_column_meta( $form_id );
		if ( is_array( $columns ) ) {
			$count = sizeof( $columns );
			for ( $i = $count - 1; $i >= 0; $i -- ) {
				if ( intval( rgar( $columns, $i ) ) == intval( $field_id ) ) {
					unset( $columns[ $i ] );
				}
			}
			self::update_grid_column_meta( $form_id, $columns );
		}

		self::delete_field_values( $form_id, $field_id );

		/**
		 * Fires after a field is deleted
		 *
		 * @param int $form_id  The form ID where the form was deleted
		 * @param int $field_id The ID of the field that was deleted
         *
		 */
		do_action( 'gform_after_delete_field', $form_id, $field_id );

		return $form;
	}

	/**
	 * Deletes confirmation conditional logic rules based on the deleted field.
	 *
	 * @since 2.4.6.1
	 *
	 * @param array $form     The form containing the confirmations to be processed.
	 * @param int   $field_id The ID of the field being deleted.
	 *
	 * @return array
	 */
	public static function delete_field_from_confirmations( $form, $field_id ) {
		if ( empty( $form['confirmations'] ) ) {
			return $form;
		}

		$save = false;

		foreach ( $form['confirmations'] as &$confirmation ) {
			if ( ! empty( $confirmation['conditionalLogic'] ) ) {
				$processed = self::delete_field_from_conditional_logic( $confirmation['conditionalLogic'], $field_id );
				if ( $confirmation['conditionalLogic'] != $processed ) {
					$confirmation['conditionalLogic'] = $processed;
					$save                             = true;
				}
			}
		}

		if ( $save ) {
			GFFormsModel::update_form_meta( $form['id'], $form['confirmations'], 'confirmations' );
		}

		return $form;
	}

	/**
	 * Deletes notification routing and conditional logic rules based on the deleted field.
	 *
	 * @since 2.4.6.1
	 *
	 * @param array $form     The form containing the notifications to be processed.
	 * @param int   $field_id The ID of the field being deleted.
	 *
	 * @return array
	 */
	public static function delete_field_from_notifications( $form, $field_id ) {
		if ( empty( $form['notifications'] ) ) {
			return $form;
		}

		$save = false;

		foreach ( $form['notifications'] as &$notification ) {
			if ( ! empty( $notification['routing'] ) ) {
				$dirty = false;

				foreach ( $notification['routing'] as $key => $rule ) {
					if ( intval( rgar( $rule, 'fieldId' ) ) == $field_id ) {
						unset( $notification['routing'][ $key ] );
						$dirty = true;
					}
				}

				if ( $dirty ) {
					$notification['routing'] = empty( $notification['routing'] ) ? null : array_values( $notification['routing'] );
					$save                    = true;
				}
			}

			if ( ! empty( $notification['conditionalLogic'] ) ) {
				$processed = self::delete_field_from_conditional_logic( $notification['conditionalLogic'], $field_id );
				if ( $notification['conditionalLogic'] != $processed ) {
					$notification['conditionalLogic'] = $processed;
					$save                             = true;
				}
			}
		}

		if ( $save ) {
			GFFormsModel::update_form_meta( $form['id'], $form['notifications'], 'notifications' );
		}

		return $form;
	}

	/**
	 * Deletes conditional logic rules based on the deleted field.
	 *
	 * If no rules remain following the deletion conditional logic is disabled.
	 *
	 * @since 2.4.6.1
	 *
	 * @param array $logic    The conditional logic object to be processed.
	 * @param int   $field_id The ID of the field being deleted.
	 *
	 * @return null|array
	 */
	public static function delete_field_from_conditional_logic( $logic, $field_id ) {
		if ( empty( $logic['rules'] ) ) {
			return null;
		}

		$dirty = false;

		foreach ( $logic['rules'] as $key => $rule ) {
			if ( intval( rgar( $rule, 'fieldId' ) ) == $field_id ) {
				unset( $logic['rules'][ $key ] );
				$dirty = true;
			}
		}

		if ( $dirty ) {
			if ( empty( $logic['rules'] ) ) {
				$logic = null;
			} else {
				$logic['rules'] = array_values( $logic['rules'] );
			}
		}

		return $logic;
	}

	public static function delete_field_values( $form_id, $field_id ) {
		global $wpdb;

		if ( version_compare( self::get_database_version(), '2.3-dev-1', '<' ) ) {
			GF_Forms_Model_Legacy::delete_field_values( $form_id, $field_id );
			return;
		}

		$entry_table      = self::get_entry_table_name();
		$entry_meta_table = self::get_entry_meta_table_name();

		// Delete from entry meta
		$sql = $wpdb->prepare( "DELETE FROM %i WHERE form_id=%d AND meta_key = %s", $entry_meta_table, $form_id, $field_id );
		if ( is_numeric( $field_id ) ) {
			$sql .= $wpdb->prepare( " OR form_id=%d AND meta_key LIKE %s", $form_id, sprintf( '%d.%%', $field_id ) );
		}
		$wpdb->query( $sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared

		// Delete leads with no details
		$sql = $wpdb->prepare(
			" DELETE FROM %i
	            WHERE form_id=%d
	            AND id NOT IN(
	                SELECT DISTINCT(entry_id) FROM %i WHERE form_id=%d
	            )", $entry_table, $form_id, $entry_meta_table, $form_id
		);
		$wpdb->query( $sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
	}

	/**
	 * Deletes a lead.
	 *
	 * @param $lead_id
	 */
	public static function delete_lead( $lead_id ) {
		self::delete_entry( $lead_id );
	}

	public static function delete_entry( $entry_id ) {
		global $wpdb, $current_user;

		// Log if user requested deletion of entries
		if ( ! empty( $current_user->user_login ) ) {
			GFCommon::log_debug( __METHOD__ . "(): User ID {$current_user->ID} requested deletion of entry #{$entry_id}" );
		}

		if ( version_compare( GFFormsModel::get_database_version(), '2.3-dev-1', '<' ) ) {
			GF_Forms_Model_Legacy::delete_lead( $entry_id );
			return;
		}

		GFCommon::log_debug( __METHOD__ . "(): Deleting entry #{$entry_id}." );

		/**
		 * Fires before an entry is deleted.
		 *
		 * @param $entry_id
		 */
		do_action( 'gform_delete_entry', $entry_id );

		/**
		 * Fires before a lead is deleted
		 * @param $lead_id
		 * @deprecated Use gform_delete_entry instead
		 * @see gform_delete_entry
		 * @remove-in 3.0
		 */

		if ( has_action( 'gform_delete_lead' ) ) {
			trigger_error( 'The gform_delete_lead action is deprecated and will be removed in version 3.0. Use gform_delete_entry instead.', E_USER_DEPRECATED ); // phpcs:ignore QITStandard.PHP.DebugCode.DebugFunctionFound
		}
		do_action( 'gform_delete_lead', $entry_id );


		$entry_table             = self::get_entry_table_name();
		$entry_notes_table       = self::get_entry_notes_table_name();
		$entry_meta_table_name = self::get_entry_meta_table_name();

		// Deleting uploaded files
		self::delete_files( $entry_id );

		// Delete from entry meta
		$sql = $wpdb->prepare( "DELETE FROM %i WHERE entry_id=%d", $entry_meta_table_name, $entry_id );
		$wpdb->query( $sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared

		// Delete from lead notes
		$sql = $wpdb->prepare( "DELETE FROM %i WHERE entry_id=%d", $entry_notes_table, $entry_id );
		$wpdb->query( $sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared


		// Delete from entry table
		$sql = $wpdb->prepare( "DELETE FROM %i WHERE id=%d", $entry_table, $entry_id );
		$wpdb->query( $sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
	}

	/**
	 * Adds a note.
	 *
	 * @since 2.4.18 Return statement added.
	 * @since unknown
	 *
	 * @param int     $entry_id  ID of the entry to add the note to.
	 * @param int     $user_id   ID of the user who created the note.
	 * @param string  $user_name Name of the user who created the note.
	 * @param string  $note      Text of the note.
	 * @param string  $note_type Note type.
	 * @param null    $sub_type  Note sub-type.
	 * @return int               ID of the new note.
	 */
	public static function add_note( $entry_id, $user_id, $user_name, $note, $note_type = 'user', $sub_type = null ) {
		global $wpdb;

		if ( version_compare( GFFormsModel::get_database_version(), '2.3-dev-1', '<' ) ) {
			GF_Forms_Model_Legacy::add_note( $entry_id, $user_id, $user_name, $note, $note_type );
			return;
		}

		$table_name = self::get_entry_notes_table_name();
		$sql        = $wpdb->prepare( "INSERT INTO %i(entry_id, user_id, user_name, value, note_type, sub_type, date_created) values(%d, %d, %s, %s, %s, %s, utc_timestamp())", $table_name, $entry_id, $user_id, $user_name, $note, $note_type, $sub_type );

		$wpdb->query( $sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared

		/**
		 * Fires after a note has been added to an entry
		 *
		 * @since 2.4.13  Added sub_type parameter.
		 * @since Unknown
		 *
		 * @param int    $wpdb->insert_id The row ID of this note in the database
		 * @param int    $entry_id        The ID of the entry that the note was added to
		 * @param int    $user_id         The ID of the current user adding the note
		 * @param string $user_name       The user name of the current user
		 * @param string $note            The content of the note being added
		 * @param string $note_type       The type of note being added.  Defaults to 'note'
		 * @param string $sub_type        The sub-type of note being added.
		 *
		 */
		do_action( 'gform_post_note_added', $wpdb->insert_id, $entry_id, $user_id, $user_name, $note, $note_type, $sub_type );

		return $wpdb->insert_id;
	}

	/**
	 * Updates a note.
	 *
	 * @since 2.4.18
	 *
	 * @param int       $note_id      ID of the note to update.
	 * @param int       $entry_id     ID of the entry to add the note to.
	 * @param int       $user_id      ID of the user who created the note.
	 * @param string    $user_name    Name of the user who created the note.
	 * @param string    $date_created Date and time the note was created in SQL datetime format.
	 * @param string    $note         Text of the note.
	 * @param string    $note_type    Note type.
	 * @param null      $sub_type     Note sub-type.
	 * @return string
	 */
	public static function update_note( $note_id, $entry_id, $user_id, $user_name, $date_created, $note, $note_type = 'user', $sub_type = null ) {
		global $wpdb;

		$table_name = self::get_entry_notes_table_name();
		$sql = $wpdb->prepare(
			"UPDATE %i SET
                entry_id = %d,
                user_id = %d,
                user_name = %s,
                date_created = %s,
                value = %s,
                note_type = %s,
                sub_type = %s
                WHERE
                id = %d
                ", $table_name, $entry_id, $user_id, $user_name, $date_created, $note, $note_type, $sub_type, $note_id
		);

		$result = $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared

		return $result;
	}

	/**
	 * Deletes a note.
	 *
	 * @since 2.4.18 Return statement added.
	 * @since unknown
	 *
	 * @param $note_id  int  ID of the note to delete.
	 * @return bool|int Success or failure.
	 */
	public static function delete_note( $note_id ) {
		global $wpdb;

		if ( version_compare( GFFormsModel::get_database_version(), '2.3-dev-1', '<' ) ) {
			GF_Forms_Model_Legacy::delete_note( $note_id );
			return;
		}

		$table_name = self::get_entry_notes_table_name();

		$lead_id = $wpdb->get_var( $wpdb->prepare( "SELECT entry_id FROM %i WHERE id = %d", $table_name, $note_id ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

		/**
		 * Fires before a note is deleted
		 *
		 * @param int $note_id The current note ID
		 * @param int $lead_id The current lead ID
		 */
		do_action( 'gform_pre_note_deleted', $note_id, $lead_id );

		$sql    = $wpdb->prepare( "DELETE FROM %i WHERE id=%d", $table_name, $note_id );
		$result = $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared

		return $result;
	}

	public static function delete_notes( $notes ) {
		if ( ! is_array( $notes ) ) {
			return;
		}

		foreach ( $notes as $note_id ) {
			self::delete_note( $note_id );
		}
	}

	/**
	 * Add note to entry containing the notification sending result
	 *
	 * @since  2.4.14
	 *
	 * @param integer        $entry_id       Id number for entry being processed.
	 * @param string|boolean $result         The result returned by wp_mail().
	 * @param array          $notification   The notification properties.
	 * @param string         $error_info     Additional details for notifications with error.
	 * @param array          $email          Array containing email details.
	 * @param array          $note_args      Array containing text, type and subtype for the note.
	 */
	public static function add_notification_note( $entry_id, $result, $notification, $error_info = '', $email = array(), $note_args = array() ) {

		// Skip if no entry id (e.g. Save and Continue notifications).
		if ( empty( $entry_id ) ) {
			return;
		}

		// If $note_args is empty, use default arguments for Gravity Forms core notifications.
		if ( empty( $note_args ) ) {
			if ( $result === true ) {

				$note_args['type'] = 'notification';
				$note_args['subtype'] = 'success';
				$note_args['text'] = esc_html__( 'WordPress successfully passed the notification email to the sending server.', 'gravityforms' );

			} elseif ( $result === false ) {

				$note_args['type'] = 'notification';
				$note_args['subtype'] = 'error';
				$note_args['text'] = esc_html__( 'WordPress was unable to send the notification email.', 'gravityforms' );

				// Add additional error message if any.
				if ( ! empty( $error_info ) ) {
					$note_args['text'] .=  ' ' . $error_info;
				}
			}
		}

		/**
		 * Allow customization of the Sending Result Note.
		 *
		 * @param array  $note_args        Array containing text, type and subtype for the note.
		 * @param int    $entry_id         Id number for entry being processed.
		 * @param bool   $result           The result returned by wp_mail().
		 * @param array  $notification     The notification properties.
		 * @param string $error_info       Additional details for notifications with error.
		 * @param array  $email            Array containing email details.
		 */
		$note_args = apply_filters( 'gform_notification_note', $note_args, $entry_id, $result, $notification, $error_info, $email );

		if ( ! empty( $note_args['text'] ) ) {
			// translators: Notification name followed by its ID. e.g. Admin Notification (ID: 5d4c0a2a37204).
			self::add_note( $entry_id, 0, sprintf( esc_html__( '%1$s (ID: %2$s)', 'gravityforms' ), $notification['name'], $notification['id'] ), $note_args['text'], $note_args['type'], $note_args['subtype'] );
		}

	}

	/**
	 * Gets the IP to be used within the entry.
	 *
	 * @since 2.2 Using $_SERVER['REMOTE_ADDR'].
	 *
	 * @return string The IP to be stored in the entry.
	 */
	public static function get_ip() {

		$ip = rgar( $_SERVER, 'REMOTE_ADDR' );

		/**
		 * Allows the IP address of the client to be modified.
		 *
		 * Use this filter if the server is behind a proxy.
		 *
		 * @since 2.2
		 * @example https://docs.gravityforms.com/gform_ip_address/
		 *
		 * @param string $ip The IP being used.
		 */
		$ip = apply_filters( 'gform_ip_address', $ip );

		// HTTP_X_FORWARDED_FOR can return a comma separated list of IPs; use the first one
		$ips = explode( ',', $ip );

		return $ips[0];
	}

	public static function save_lead( $form, &$entry ) {
		self::save_entry( $form, $entry );
	}

	/**
	 * Save Entry to database.
	 *
	 * @since 2.4.8.13 Updated created_by property to save as an empty value when undefined.
	 * @since Unknown
	 *
	 * @param array $form  Form object.
	 * @param array $entry Entry object.
	 */
	public static function save_entry( $form, &$entry ) {
		global $wpdb, $current_user;

		if ( version_compare( self::get_database_version(), '2.3-dev-1', '<' ) ) {
			GF_Forms_Model_Legacy::save_lead( $form, $entry );
			$entry = GFAPI::get_entry( $entry['id'] );
			return;
		}

		GFCommon::timer_start( __METHOD__ );
		GFCommon::log_debug( __METHOD__ . '(): Saving entry.' );

		$is_form_editor  = GFCommon::is_form_editor();
		$is_entry_detail = GFCommon::is_entry_detail();
		$is_admin        = $is_form_editor || $is_entry_detail;

		if ( $is_admin && ! GFCommon::current_user_can_any( 'gravityforms_edit_entries' ) ) {
			wp_die( esc_html__( "You don't have adequate permission to edit entries.", 'gravityforms' ) );
		}

		$entry_meta_table = self::get_entry_meta_table_name();
		$is_new_lead      = empty( $entry );

		// Log user login for user updating the entry
		if ( ! $is_new_lead && ! empty( $entry['id'] ) && ! empty( $current_user->ID ) ) {
			GFCommon::log_debug( __METHOD__ . "(): User ID {$current_user->ID} requested update of entry #{$entry['id']}." );
		}

		if ( ! $is_new_lead && ! self::entry_exists( rgar( $entry, 'id' ) ) ) {
			// Force a new entry to be saved when an entry does not exist for the supplied id.
			$entry       = array();
			$is_new_lead = true;
			GFCommon::log_debug( __METHOD__ . '(): Existing entry not found. Saving new entry instead.' );
		}

		$die_message = esc_html__( 'An error prevented the entry for this form submission being saved. Please contact support.', 'gravityforms' );

		$entry_table = GFFormsModel::get_entry_table_name();

		$current_date = $wpdb->get_var( 'SELECT utc_timestamp()' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

		if ( $is_new_lead ) {
			// Saving the new entry.

			$user_id = $current_user && $current_user->ID ? $current_user->ID : null;

			$user_agent = self::truncate( rgar( $_SERVER, 'HTTP_USER_AGENT' ), 250 );
			$user_agent = sanitize_text_field( $user_agent );
			$source_url = self::truncate( self::get_current_page_url(), 200 );

			/**
			 * Allow the currency code to be overridden.
			 *
			 * @param string $currency The three character ISO currency code to be stored in the entry. Default is value returned by GFCommon::get_currency()
			 * @param array $form The form currently being processed.
			 *
			 */
			$currency = gf_apply_filters( array( 'gform_currency_pre_save_entry', $form['id'] ), GFCommon::get_submission_currency(), $form );

			$ip        = rgars( $form, 'personalData/preventIP' ) ? '' : self::get_ip();
			$source_id = self::get_source_id( $form );

			$wpdb->insert( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
				$entry_table,
				array(
					'form_id'      => $form['id'],
					'ip'           => $ip,
					'source_url'   => $source_url,
					'date_created' => $current_date,
					'date_updated' => $current_date,
					'user_agent'   => $user_agent,
					'currency'     => $currency,
					'created_by'   => $user_id,
					'source_id'    => $source_id,
				),
				array(
					'form_id'      => '%d',
					'ip'           => '%s',
					'source_url'   => '%s',
					'date_created' => '%s',
					'date_updated' => '%s',
					'user_agent'   => '%s',
					'currency'     => '%s',
					'created_by'   => '%s',
					'source_id'    => '%d',
				)
			);

			// Reading newly created lead id
			$lead_id = $wpdb->insert_id;

			if ( $lead_id == 0 ) {
				GFCommon::log_error( __METHOD__ . '(): Unable to save entry. ' . $wpdb->last_error );
				wp_die( esc_html( $die_message ) );
			}

			$entry = array(
				'id'               => (string) $lead_id,
				'status'           => 'active',
				'form_id'          => (string) $form['id'],
				'ip'               => $ip,
				'source_url'       => $source_url,
				'currency'         => $currency,
				'post_id'          => null,
				'date_created'     => $current_date,
				'date_updated'     => $current_date,
				'is_starred'       => 0,
				'is_read'          => 0,
				'user_agent'       => $user_agent,
				'payment_status'   => null,
				'payment_date'     => null,
				'payment_amount'   => null,
				'payment_method'   => '',
				'transaction_id'   => null,
				'is_fulfilled'     => null,
				'created_by'       => (string) $user_id,
				'transaction_type' => null,
				'source_id'        => $source_id,
			);

			GFCommon::log_debug( __METHOD__ . "(): Entry record created in the database. ID: {$lead_id}." );
		} else {
			GFCommon::log_debug( __METHOD__ . "(): Updating existing entry. ID: {$entry['id']}." );

			// Ensures the entry being updated contains all the current properties and registered meta.
			self::add_properties_to_entry( $entry );
			self::add_meta_to_entry( $entry );

			GFAPI::update_entry_property( $entry['id'], 'date_updated', $current_date );
			$entry['date_updated'] = $current_date;
		}

		$current_fields = $wpdb->get_results( $wpdb->prepare( "SELECT id, meta_key, item_index FROM %i WHERE entry_id=%d", $entry_meta_table, $entry['id'] ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$total_fields = array();
		/* @var $calculation_fields GF_Field[] */
		$calculation_fields = array();
		$has_post_field     = false;

		GFCommon::log_debug( __METHOD__ . '(): Saving entry fields.' );

		GFFormsModel::begin_batch_field_operations();

		foreach ( $form['fields'] as $field ) {
			/* @var $field GF_Field */

			// ignore the honeypot field
			if ( $field->type == 'honeypot' ) {
				continue;
			}

			//Ignore fields that are marked as display only
			if ( $field->displayOnly ) {
				continue;
			}

			// Ignore pricing fields in the entry detail
			if ( $is_entry_detail && GFCommon::is_pricing_field( $field->type ) ) {
				continue;
			}


			// Process total field after all fields have been saved
			if ( $field->type == 'total' ) {
				$total_fields[] = $field;
				continue;
			}

			/**
			 * Specify whether to fetch values from the $_POST when evaluating a field's conditional logic. Defaults to true
			 * for new entries and false for existing entries.
			 *
			 * @since 2.3.1.11
			 *
			 * @param bool  $read_value_from_post Should value be fetched from $_POST?
			 * @param array $form                The current form object.
			 * @param array $entry               The current entry object.
			 */
			$read_value_from_post = gf_apply_filters( array( 'gform_use_post_value_for_conditional_logic_save_entry', $form['id'] ), $is_new_lead || ! isset( $entry[ 'date_created' ] ), $form, $entry );

			// Only save fields that are not hidden (except when updating an entry)
			if ( $is_entry_detail || ! GFFormsModel::is_field_hidden( $form, $field, array(), $read_value_from_post ? null : $entry ) ) {

				// process calculation fields after all fields have been saved (moved after the is hidden check)
				if ( $field->has_calculation() ) {
					$calculation_fields[] = $field;
					continue;
				}

				if ( $field->type === 'post_category' ) {
					$field          = GFCommon::add_categories_as_choices( $field, '' );
					$has_post_field = true;
				} elseif ( ! $has_post_field && GFCommon::is_post_field( $field ) ) {
					$has_post_field = true;
				}

				$inputs = $field->get_entry_inputs();
				if ( is_array( $inputs ) ) {
					foreach ( $inputs as $input ) {
						self::save_input( $form, $field, $entry, $current_fields, $input['id'] );
					}
				} else {
					self::save_input( $form, $field, $entry, $current_fields, $field->id );
				}
			}
		}

		GFCache::set( 'has_post_field_entry_' . $entry['id'], $has_post_field );

		$results = GFFormsModel::commit_batch_field_operations();

		if ( $is_new_lead && is_wp_error( $results['inserts'] ) ) {
			/* @var WP_Error $error */
			$error = $results['inserts'];
			GFCommon::log_error( __METHOD__ . '(): Error while saving field values for new entry. ' . $error->get_error_message() );
			self::add_note( $entry['id'], 0, 'Gravity Forms', sprintf( esc_html__( 'Error while saving field values: %s', 'gravityforms' ), $error->get_error_message() ), 'save_entry', 'error' );
			wp_die( esc_html( $die_message ) );
		}

		if ( ! empty( $calculation_fields ) ) {
			GFFormsModel::begin_batch_field_operations();
			foreach ( $calculation_fields as $calculation_field ) {
				$inputs = $calculation_field->get_entry_inputs();
				if ( is_array( $inputs ) ) {
					foreach ( $inputs as $input ) {
						self::save_input( $form, $calculation_field, $entry, $current_fields, $input['id'] );
					}
				} else {
					self::save_input( $form, $calculation_field, $entry, $current_fields, $calculation_field->id );
				}
			}
			$results = GFFormsModel::commit_batch_field_operations();

			if ( $is_new_lead && is_wp_error( $results['inserts'] ) ) {
				/* @var WP_Error $error */
				$error = $results['inserts'];
				GFCommon::log_error( __METHOD__ . '(): Error while saving calculation field values for new entry. ' . $error->get_error_message() );
				self::add_note( $entry['id'], 0, 'Gravity Forms', sprintf( esc_html__( 'Error while saving calculation field values: %s', 'gravityforms' ), $error->get_error_message() ), 'save_entry', 'error' );
				wp_die( esc_html( $die_message ) );
			}

			self::refresh_product_cache( $form, $entry );
		}

		//saving total field as the last field of the form.
		if ( ! empty( $total_fields ) ) {
			GFFormsModel::begin_batch_field_operations();
			foreach ( $total_fields as $total_field ) {
				self::save_input( $form, $total_field, $entry, $current_fields, $total_field->id );
			}
			$results = GFFormsModel::commit_batch_field_operations();

			if ( $is_new_lead && is_wp_error( $results['inserts'] ) ) {
				/* @var WP_Error $error */
				$error = $results['inserts'];
				GFCommon::log_error( __METHOD__ . '(): Error while saving total field values for new entry. ' . $error->get_error_message() );
				self::add_note( $entry['id'], 0, 'Gravity Forms', sprintf( esc_html__( 'Error while saving total field values: %s', 'gravityforms' ), $error->get_error_message() ), 'save_entry', 'error' );
				wp_die( esc_html( $die_message ) );
			}
		}

		$processor = self::get_entry_meta_batch_processor();
		$processor::begin_batch_entry_meta_operations();

		foreach ( $form['fields'] as $field ) {
			/* @var GF_Field $field */

			if ( $field->displayOnly ) {
				continue;
			}

			$inputs = $field->get_entry_inputs();

			if ( is_array( $inputs ) ) {
				foreach ( $inputs as $input ) {
					$entry[ (string) $input['id'] ] = gf_apply_filters( array( 'gform_get_input_value', $form['id'], $field->id, $input['id'] ), rgar( $entry, (string) $input['id'] ), $entry, $field, $input['id'] );
				}
			} else {

				$value = rgar( $entry, (string) $field->id );

				if ( GFFormsModel::is_openssl_encrypted_field( $entry['id'], $field->id ) ) {
					$value = GFCommon::openssl_decrypt( $value );
				}

				$entry[ (string) $field->id ] = gf_apply_filters( array( 'gform_get_input_value', $form['id'], $field->id ), $value, $entry, $field, '' );

			}

			self::save_extra_field_meta( $field, $form, $entry );

		}

		$processor::commit_batch_entry_meta_operations();

		self::hydrate_repeaters( $entry, $form );

		GFCommon::log_debug( __METHOD__ . '(): Finished saving entry fields.' );
		GFCommon::log_debug( __METHOD__ . sprintf( '(): %s entry completed in %F seconds.', $is_new_lead ? 'Saving' : 'Updating', GFCommon::timer_end( __METHOD__ ) ) );
	}

	/**
	 * Helper to get the ID of the post or page where the form submission originated.
	 *
	 * @since 2.9.0
	 *
	 * @param array $form The form the entry is being created for.
	 *
	 * @return int|null
	 */
	private static function get_source_id( $form ) {
		if ( defined( 'DOING_AJAX' ) && DOING_AJAX && ! empty( $_SERVER['HTTP_REFERER'] ) ) {
			$id = url_to_postid( $_SERVER['HTTP_REFERER'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
		} elseif ( rgget( 'gf_page' ) === 'preview' ) {
			return null;
		} elseif ( is_singular() ) {
			$id = get_the_ID();
		}

		/**
		 * Allows the value to be saved to the entry source_id property to be overridden.
		 *
		 * @since 2.9
		 *
		 * @param int|null  $id     The ID of the post or page where the form submission originated.
		 * @param array     $form   The form the entry is being created for.
		 */
		return gf_apply_filters( array( 'gform_source_id_pre_save_entry', (int) rgar( $form, 'id' ) ), ! empty( $id ) ? $id : null, $form );
	}

	/**
	 * Gets the extra meta data a field wants to save to the entry and updates the entry meta with the retrieved data.
	 *
	 * This method depends on batch operations, so begin_batch_entry_meta_operations should be called before this method is called
	 * and commit_batch_entry_meta_operations should be called after.
	 *
	 * @since 2.5.16
	 *
	 * @param GF_Field $field The current field.
	 * @param array    $form  The form that contains the field.
	 * @param array    $entry The entry that was saved.
	 */
	public static function save_extra_field_meta( $field, $form, $entry ) {

		$extra_meta = $field->get_extra_entry_metadata( $form, $entry );
		if ( empty( $extra_meta ) ) {
			return;
		}

		$processor = self::get_entry_meta_batch_processor();

		foreach ( $extra_meta as $key => $value ) {
			$processor::queue_batch_entry_meta_operation( $form, $entry, $key, $value );
		}
	}

	/**
	 * Creates a new instance of the entry meta batch operations handler if required and returns it.
	 *
	 * @since 2.5.16
	 *
	 * @return GF_Entry_Meta_Batch_Processor
	 */
	public static function get_entry_meta_batch_processor() {
		if ( is_null( self::$entry_meta_batch_processor ) ) {
			$processor = GFForms::get_service_container()->get( GF_Batch_Operations_Service_Provider::ENTRY_META_BATCH_PROCESSOR );
			self::$entry_meta_batch_processor = $processor;
		}

		return self::$entry_meta_batch_processor;
	}

	/**
	 * Populates the supplied entry with missing properties.
	 *
	 * @since 2.4.5.8
	 *
	 * @param array $entry The partial or complete entry currently being updated.
	 */
	public static function add_properties_to_entry( &$entry ) {
		if ( empty( $entry['id'] ) ) {
			return;
		}

		global $wpdb;
		$entry_table = GFFormsModel::get_entry_table_name();
		$sql         = $wpdb->prepare( "SELECT * FROM %i WHERE id=%d", $entry_table, $entry['id'] );
		$properties  = $wpdb->get_row( $sql, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

		foreach ( $properties as $key => $property ) {
			if ( ! isset( $entry[ (string) $key ] ) ) {
				// Add the missing entry property.
				$entry[ (string) $key ] = $properties[ $key ];
			}
		}
	}

	/**
	 * Populates the supplied entry with missing meta.
	 *
	 * @since 2.4.5.8
	 *
	 * @param array $entry The partial or complete entry currently being updated.
	 */
	public static function add_meta_to_entry( &$entry ) {
		if ( empty( $entry['id'] ) || empty( $entry['form_id'] ) ) {
			return;
		}

		$meta_keys = array_keys( self::get_entry_meta( $entry['form_id'] ) );

		foreach ( $meta_keys as $meta_key ) {
			if ( ! isset( $entry[ $meta_key ] ) ) {
				// Add the missing entry meta.
				$entry[ $meta_key ] = gform_get_meta( $entry['id'], $meta_key );
			}
		}
	}

	/**
	 * Fill the repeater field with the entry data.
	 *
	 * @since Unknown.
	 * @since 2.5 Added the $apply_filters parameter.
	 *
	 * @param       $entry
	 * @param       $form
	 * @param bool  $apply_filters Whether to apply the filter_input_value filter to the entry.
	 */
	public static function hydrate_repeaters( &$entry, $form, $apply_filters = false ) {
		$fields = $form['fields'];
		foreach ( $fields as $field ) {
			if ( $field instanceof GF_Field_Repeater && isset( $field->fields ) && is_array( $field->fields ) ) {
				/* @var GF_Field_Repeater $field */
				$entry = $field->hydrate( $entry, $form );
			}
		}
	}

	public static function create_lead( $form ) {
		GFCommon::timer_start( __METHOD__ );
		$form_id = absint( rgar( $form, 'id' ) );

		global $current_user;

		$total_fields       = array();
		$calculation_fields = array();

		$lead                 = array();
		$lead['id']           = null;
		$lead['post_id']      = null;
		$lead['date_created'] = null;
		$lead['date_updated'] = null;
		$lead['form_id']      = rgar( $form, 'id' );
		$lead['ip']           = rgars( $form, 'personalData/preventIP' ) ? '' : self::get_ip();
		$source_url           = self::truncate( self::get_current_page_url(), 200 );
		$lead['source_url']   = esc_url_raw( $source_url );
		$user_agent           = self::truncate( rgar( $_SERVER, 'HTTP_USER_AGENT' ), 250 );
		$lead['user_agent']   = sanitize_text_field( $user_agent );
		$lead['created_by']   = $current_user && $current_user->ID ? $current_user->ID : 'NULL';
		$lead['source_id']    = self::get_source_id( $form );

		/**
		 * Allow the currency code to be overridden.
		 *
		 * @param string $currency The three character ISO currency code to be stored in the entry. Default is value returned by GFCommon::get_currency()
		 * @param array $form The form currently being processed.
		 *
		 */
		$lead['currency'] = gf_apply_filters( array( 'gform_currency_pre_save_entry', $form_id ), GFCommon::get_submission_currency(), $form );

		foreach ( $form['fields'] as $field ) {
			/* @var $field GF_Field */

			// ignore fields that are marked as display only
			if ( $field->displayOnly ) {
				continue;
			}

			// process total field after all fields have been saved
			if ( $field->type == 'total' ) {
				$total_fields[] = $field;
				continue;
			}

			// process calculation fields after all fields have been saved
			if ( $field->has_calculation() ) {
				$calculation_fields[] = $field;
				continue;
			}

			// only save fields that are not hidden
			if ( ! RGFormsModel::is_field_hidden( $form, $field, array() ) ) {

				if ( $field->type == 'post_category' ) {
					$field = GFCommon::add_categories_as_choices( $field, '' );
				}

				$inputs = $field->get_entry_inputs();

				if ( is_array( $inputs ) ) {
					foreach ( $inputs as $input ) {
						$lead[ (string) $input['id'] ] = self::get_prepared_input_value( $form, $field, $lead, $input['id'] );
					}
				} else {
					$lead[ $field->id ] = self::get_prepared_input_value( $form, $field, $lead, $field->id );
				}
			}
		}

		if ( ! empty( $calculation_fields ) ) {
			foreach ( $calculation_fields as $field ) {
				/* @var $field GF_Field */

				// only save fields that are not hidden
				if ( RGFormsModel::is_field_hidden( $form, $field, array() ) ) {
					continue;
				}

				$inputs = $field->get_entry_inputs();

				if ( is_array( $inputs ) ) {
					foreach ( $inputs as $input ) {
						$lead[ (string) $input['id'] ] = self::get_prepared_input_value( $form, $field, $lead, $input['id'] );
					}
				} else {
					$lead[ $field->id ] = self::get_prepared_input_value( $form, $field, $lead, $field->id );
				}
			}
			self::refresh_product_cache( $form, $lead );
		}

		// saving total field as the last field of the form.
		if ( ! empty( $total_fields ) ) {
			foreach ( $total_fields as $total_field ) {
				$lead[ $total_field->id ] = self::get_prepared_input_value( $form, $total_field, $lead, $total_field->id );
			}
		}

		GFCommon::log_debug( __METHOD__ . sprintf( '(): Draft entry created for form (#%d) in %F seconds.', $form_id, GFCommon::timer_end( __METHOD__ ) ) );

		return $lead;
	}

	public static function get_prepared_input_value( $form, $field, $lead, $input_id ) {

		$input_name = 'input_' . str_replace( '.', '_', $input_id );
		if ( $field->enableCopyValuesOption && rgpost( 'input_' . $field->id . '_copy_values_activated' ) ) {
			$source_field_id   = $field->copyValuesOptionField;
			$source_input_name = str_replace( 'input_' . $field->id, 'input_' . $source_field_id, $input_name );
			$value             = rgpost( $source_input_name );
		} else {
			$value = rgpost( $input_name );
		}

		$form_id = absint( rgar( $form, 'id' ) );
		$value   = self::maybe_trim_input( $value, $form_id, $field );

		$is_form_editor  = GFCommon::is_form_editor();
		$is_entry_detail = GFCommon::is_entry_detail();
		$is_admin        = $is_form_editor || $is_entry_detail;

		if ( empty( $value ) && $field->is_administrative() && ! $is_admin ) {
			$value = self::get_default_value( $field, $input_id );
		}

		switch ( self::get_input_type( $field ) ) {

			case 'post_image':
				/** @var GF_Field_Post_Image $field */
				$files = $field->get_submission_files();
				if ( $field->is_submission_files_empty( $files ) ) {
					$value = '';
					break;
				}

				$file_info = ! empty( $files['existing'] ) ? $files['existing'][0] : $field->get_tmp_file_details( $files['new'][0] );

				if ( ! empty( $file_info ) ) {
					if ( isset( $file_info['url'] ) ) {
						$url = $file_info['url'];
					} else {
						$file_path = self::get_file_upload_path( $form_id, $file_info['uploaded_filename'] );
						$url       = $file_path['url'];
					}

					$image_alt         = isset( $_POST[ "{$input_name}_2" ] ) ? strip_tags( $_POST[ "{$input_name}_2" ] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
					$image_title       = isset( $_POST[ "{$input_name}_1" ] ) ? strip_tags( $_POST[ "{$input_name}_1" ] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
					$image_caption     = isset( $_POST[ "{$input_name}_4" ] ) ? strip_tags( $_POST[ "{$input_name}_4" ] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
					$image_description = isset( $_POST[ "{$input_name}_7" ] ) ? strip_tags( $_POST[ "{$input_name}_7" ] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized

					$value = ! empty( $url ) ? $url . '|:|' . $image_title . '|:|' . $image_caption . '|:|' . $image_description . '|:|' . $image_alt : '';
				}
				break;

			case 'fileupload':
				/** @var GF_Field_FileUpload $field */
				$files = $field->get_submission_files();
				if ( $field->is_submission_files_empty( $files ) ) {
					$value = '';
					break;
				}

				$tmp_location = GFFormsModel::get_tmp_upload_location( $form_id );
				$tmp_path     = rgar( $tmp_location, 'path' );
				$tmp_url      = rgar( $tmp_location, 'url' );
				$value        = array(); // Initialize as empty array to store file info

				foreach ( $files['existing'] as $file ) {
					if ( empty( $file['temp_filename'] ) && empty( $file['uploaded_filename'] ) && empty( $file['url'] ) ) {
						continue;
					}

					$name    = rgar( $file, 'temp_filename' );
					$name    = ! empty( $name ) ? sanitize_file_name( wp_basename( $name ) ) : '';
					$value[] = array(
						'tmp_path'      => $name ? $tmp_path . $name : '',
						'tmp_url'       => rgar( $file, 'url', $name ? $tmp_url . $name : '' ),
						'tmp_name'      => $name,
						'uploaded_name' => rgar( $file, 'uploaded_filename' ),
					);

					if ( ! $field->multipleFiles && count( $value ) === 1 ) {
						break;
					}
				}

				$new_file_updated = false;
				foreach ( $files['new'] as &$file ) {
					if ( ! $field->multipleFiles && count( $value ) === 1 ) {
						break;
					}

					$file_info = $field->get_tmp_file_details( $file );
					if ( empty( $file_info ) ) {
						continue;
					}

					if ( ! isset( $file['details'] ) ) {
						$file['details']  = $file_info;
						$new_file_updated = true;
					}

					$name    = rgar( $file_info, 'temp_filename' );
					$name    = ! empty( $name ) ? sanitize_file_name( wp_basename( $name ) ) : '';
					$value[] = array(
						'tmp_path'      => $name ? $tmp_path . $name : '',
						'tmp_url'       => $name ? $tmp_url . $name : '',
						'tmp_name'      => $name,
						'uploaded_name' => rgar( $file_info, 'uploaded_filename' ),
					);
				}

				if ( $new_file_updated ) {
					$field->set_submission_files( $files );
				}

				if ( ! empty( $value ) ) {
					$value = json_encode( $value ); // Encode the array of temp URLs as JSON string
				} else {
					// If no files were uploaded, set the value to an empty string for backwards compatibility
					$value = '';
				}

				break;


			default:

				// processing values so that they are in the correct format for each input type
				$value = self::prepare_value( $form, $field, $value, $input_name, rgar( $lead, 'id' ), $lead );

		}

		return gf_apply_filters( array( 'gform_save_field_value', $form_id, $field->id ), $value, $lead, $field, $form, $input_id );
	}

	public static function refresh_product_cache( $form, $lead, $use_choice_text = false, $use_admin_label = false ) {

		$cache_options = array(
			array( false, false ),
			array( false, true ),
			array( true, false ),
			array( true, true ),
		);

		foreach ( $cache_options as $cache_option ) {
			list( $use_choice_text, $use_admin_label ) = $cache_option;
			if ( gform_get_meta( rgar( $lead, 'id' ), "gform_product_info_{$use_choice_text}_{$use_admin_label}" ) ) {
				gform_delete_meta( rgar( $lead, 'id' ), "gform_product_info_{$use_choice_text}_{$use_admin_label}" );
				GFCommon::get_product_fields( $form, $lead, $use_choice_text, $use_admin_label );
			}
		}

	}

	/**
	 * Check whether a field is hidden via conditional logic.
	 *
	 * @param array    $form         Form object.
	 * @param GF_Field $field        Field object.
	 * @param array    $field_values Default field values for this form. Used when form has not yet been submitted. Pass an array if no default field values are available/required.
	 * @param array    $lead         Optional, default is null. If lead object is available, pass the lead.
	 *
	 * @return mixed
	 */
	public static function is_field_hidden( $form, $field, $field_values, $lead = null ) {

		if ( empty( $field ) ) {
			return false;
		}

		$cache_key = 'GFFormsModel::is_field_hidden_' . $form['id'] . '_' . $field->id;

		if ( ! empty( $lead ) && isset( $lead['id'] ) ) {
			// Make sure that we cache field visiblity on a per-entry basis
			// https://github.com/gravityview/GravityView/issues/1307
			$cache_key = $cache_key . '_' . $lead['id'];
		}

		$display   = GFCache::get( $cache_key, $is_hit, false );
		if ( $display !== false ) {
			return $display;
		}

		$section         = self::get_section( $form, $field->id );
		$section_display = self::get_field_display( $form, $section, $field_values, $lead );

		//if section is hidden, hide field no matter what. if section is visible, see if field is supposed to be visible
		if ( $section_display == 'hide' ) {
			$display = 'hide';
		} else if ( self::is_page_hidden( $form, $field->pageNumber, $field_values, $lead ) ) {
			$display = 'hide';
		} else {
			$display = self::get_field_display( $form, $field, $field_values, $lead );

			return $display == 'hide';
		}

		GFCache::set( $cache_key, $display );

		return $display == 'hide';
	}

	/***
	 * Determines if the submit button was supposed to be hidden by conditional logic. This function helps ensure that
	 *  the form doesn't get submitted when the submit button is hidden by conditional logic.
	 *
	 * @param array $form The Form object
	 *
	 * @return bool Returns true if the submit button is hidden by conditional logic, false otherwise.
	 */
	public static function is_submit_button_hidden( $form ) {
		if ( ! isset( $form['button']['conditionalLogic'] ) ) {
			return false;
		}

		$is_visible = self::evaluate_conditional_logic( $form, $form['button']['conditionalLogic'], array() );

		return ! $is_visible;
	}

	/**
	 * Determines if the next button was supposed to be hidden by conditional logic.
	 *
	 * @since 2.9.25
	 *
	 * @param GF_Field $field The page field containing the next button logic.
	 * @param array    $form  The current form.
	 *
	 * @return bool
	 */
	public static function is_next_button_hidden( $field, $form ) {
		if ( ! $field instanceof GF_Field_Page || ! rgars( $field->nextButton, 'conditionalLogic/enabled' ) ) {
			return false;
		}

		$is_visible = self::evaluate_conditional_logic( $form, $field->nextButton['conditionalLogic'], array() );

		return ! $is_visible;
	}

	public static function is_page_hidden( $form, $page_number, $field_values, $lead = null ) {
		$page = self::get_page_by_number( $form, $page_number );

		if ( ! $page ) {
			return false;
		}

		$display = self::get_field_display( $form, $page, $field_values, $lead );

		return $display == 'hide';
	}

	public static function get_page_by_number( $form, $page_number ) {
		foreach ( $form['fields'] as $field ) {
			if ( $field->type == 'page' && $field->pageNumber == $page_number ) {
				return $field;
			}
		}

		return null;
	}

	//gets the section that the specified field belongs to, or null if none
	public static function get_section( $form, $field_id ) {
		$current_section = null;
		foreach ( $form['fields'] as $field ) {
			if ( $field->type == 'section' ) {
				$current_section = $field;
			}

			//stop section at a page break (sections don't go cross page)
			if ( $field->type == 'page' ) {
				$current_section = null;
			}

			if ( $field->id == $field_id ) {
				return $current_section;
			}
		}

		return null;
	}

    /**
	 * Determines if the field value matches the conditional logic rule value.
	 *
	 * @param mixed         $field_value  The field value to be checked.
	 * @param mixed         $target_value The conditional logic rule value.
	 * @param string        $operation    The conditional logic rule operator.
	 * @param null|GF_Field $source_field The field the rule is based on.
	 * @param null|array    $rule         The conditional logic rule properties.
	 * @param null|array    $form         The current form.
	 *
	 * @return bool
	 */
	public static function is_value_match( $field_value, $target_value, $operation = 'is', $source_field = null, $rule = null, $form = null ) {

		$is_match = false;

		if ( $source_field && is_subclass_of( $source_field, 'GF_Field' ) ) {
			if ( $source_field->type == 'post_category' ) {
				$field_value = GFCommon::prepare_post_category_value( $field_value, $source_field, 'conditional_logic' );
			} elseif ( $source_field instanceof GF_Field_MultiSelect && ! empty( $field_value ) && ! is_array( $field_value ) ) {
				// Convert the comma-delimited string into an array.
				$field_value = $source_field->to_array( $field_value );
			} elseif ( $source_field instanceof GF_Field_Consent ) {
				// Force $rule['fieldId'] to an int because a getCorrectDefaultFieldId() bug caused some rules based on the consent field to use the input ID instead of the field ID.
				$field_value = rgar( $field_value, absint( $rule['fieldId'] ) . '.1' );
			} elseif ( $source_field->get_input_type() != 'checkbox' && is_array( $field_value ) && $source_field->id != $rule['fieldId'] && is_array( $source_field->get_entry_inputs() ) ) {
				// Get the specific input value from the full field value.
				$field_value = rgar( $field_value, $rule['fieldId'] );
			}
		}

		$form_id = $source_field instanceof GF_Field ? $source_field->formId : 0;

		$target_value = GFFormsModel::maybe_trim_input( $target_value, $form_id, $source_field );


		if ( is_array( $field_value ) ) {
			$field_value = array_values( $field_value ); // Returning array values, ignoring keys if array is associative.
			$match_count = 0;
			foreach ( $field_value as $val ) {
				$val = GFFormsModel::maybe_trim_input( GFCommon::get_selection_value( $val, $source_field ), $form_id, $source_field );
				if ( self::matches_conditional_operation( $val, $target_value, $operation ) ) {
					$match_count ++;
				}
			}

			// If operation is Is Not, none of the values in the array can match the target value.
			// Except when operation is Is Not Empty. In that case, one non-empty value is enough
			$must_match_all = ( $operation == 'isnot' && ! rgblank( $target_value ) ) || ( $operation == 'is' && rgblank( $target_value ) );
			$is_match = $must_match_all ? $match_count == count( $field_value ) : $match_count > 0;

		} else if ( self::matches_conditional_operation( GFFormsModel::maybe_trim_input( GFCommon::get_selection_value( $field_value, $source_field ), $form_id, $source_field ), $target_value, $operation ) ) {
			$is_match = true;
		}

		return apply_filters( 'gform_is_value_match', $is_match, $field_value, $target_value, $operation, $source_field, $rule );
	}

	/*
	 * @deprecated 2.9.1.  Use GFCommon::maybe_format_numeric instead.
	 *
	 * @remove-in 4.0
	 */
	private static function try_convert_float( $text ) {
		_deprecated_function( __METHOD__, '2.9.1', 'GFCommon::maybe_format_numeric' );

		/*
		global $wp_locale;
		$number_format = $wp_locale->number_format['decimal_point'] == ',' ? 'decimal_comma' : 'decimal_dot';
		if ( is_numeric( $text ) && $number_format == 'decimal_comma' ) {
			return GFCommon::format_number( $text, 'decimal_comma' );
		} else if ( GFCommon::is_numeric( $text, $number_format ) ) {
			return GFCommon::clean_number( $text, $number_format );
		}
		*/

		$number_format = 'decimal_dot';

		if ( GFCommon::is_numeric( $text, 'currency' ) ) {
			$number_format = 'currency';
		}

		if ( GFCommon::is_numeric( $text, $number_format ) ) {
			return GFCommon::clean_number( $text, $number_format );
		}

		return 0;
	}

	/*
	 * @deprecated 2.9.1.  Use GFFormsModel::matches_conditional_operation instead.
	 *
	 * @remove-in 4.0
	 */
	public static function matches_operation( $val1, $val2, $operation ) {
		_deprecated_function( __METHOD__, '2.9.1', 'GFFormsModel::matches_conditional_operation' );

		if ( in_array( $operation, array( '>', '<', 'greater_than', 'less_than' ) ) ) {
			$val1 = self::try_convert_float( $val1 );
			$val2 = self::try_convert_float( $val2 );
		}

		return self::matches_conditional_operation( $val1, $val2, $operation );
	}

	/**
	 * This method will evaluate the specified operation between the two specified values and return the result. If the two values match the operation, the method will return true. Otherwise, it will return false.
	 * The method supports the following operations: is, isnot, greater_than or >, less_than or <, contains, starts_with, ends_with.
	 *
	 * @since 2.9.1
	 *
	 * @param string $val1      The first value to be compared. Must be formatted as a valid number for greater_than and less_than operations.
	 * @param string $val2      The second value to be compared. Must be formatted as a valid number for greater_than and less_than operations.
	 * @param string $operation The operation to be performed with the specified values.
	 *
	 * @return bool Returns true if the two values match the specified operation. Otherwise, it will return false.
	 */
	public static function matches_conditional_operation( $val1, $val2, $operation ) {
		$val1 = ! rgblank( $val1 ) ? strtolower( $val1 ) : '';
		$val2 = ! rgblank( $val2 ) ? strtolower( $val2 ) : '';

		switch ( $operation ) {
			case 'is' :
				return $val1 == $val2;
				break;

			case 'isnot' :
				return $val1 != $val2;
				break;

			case 'greater_than':
			case '>' :
				return $val1 > $val2; // Do not cast these to float because this will compare numbers as well as dates.
				break;

			case 'less_than':
			case '<' :
				return $val1 < $val2; // Do not cast these to float because this will compare numbers as well as dates.
				break;

			case 'contains' :
				return ! rgblank( $val2 ) && strpos( $val1, $val2 ) !== false;
				break;

			case 'starts_with' :
				return ! rgblank( $val2 ) && strpos( $val1, $val2 ) === 0;
				break;

			case 'ends_with' :
				// If target value is a 0 set $val2 to 0 rather than the empty string it currently is to prevent false positives.
				if ( empty( $val2 ) ) {
					$val2 = '0';
				}

				$start = strlen( $val1 ) - strlen( $val2 );

				if ( $start < 0 ) {
					return false;
				}

				$tail = substr( $val1, $start );

				return $val2 == $tail;
				break;
		}

		return false;
	}

	/**
	 * @param          $form
	 * @param GF_Field $field
	 * @param          $field_values
	 * @param null     $lead
	 *
	 * @return string
	 */
	private static function get_field_display( $form, $field, $field_values, $lead = null ) {

		if ( empty( $field ) ) {
			return 'show';
		}

		$logic = $field->conditionalLogic;

		//if this field does not have any conditional logic associated with it, it won't be hidden
		if ( empty( $logic ) ) {
			return 'show';
		}

		$is_visible = self::evaluate_conditional_logic( $form, $logic, $field_values, $lead );

		return $is_visible ? 'show' : 'hide';
	}



	public static function get_custom_choices() {
		$choices = get_option( 'gform_custom_choices' );
		if ( ! $choices ) {
			$choices = array();
		}

		return $choices;
	}

	public static function delete_custom_choice( $name ) {
		$choices = self::get_custom_choices();
		if ( array_key_exists( $name, $choices ) ) {
			unset( $choices[ $name ] );
		}

		update_option( 'gform_custom_choices', $choices );
	}

	public static function save_custom_choice( $previous_name, $new_name, $choices ) {
		$all_choices = self::get_custom_choices();

		if ( array_key_exists( $previous_name, $all_choices ) ) {
			unset( $all_choices[ $previous_name ] );
		}

		$all_choices[ $new_name ] = $choices;

		update_option( 'gform_custom_choices', $all_choices );
	}


	/**
	 * Returns the value for a field.
	 *
	 * @param GF_Field $field
	 * @param array    $field_values
	 * @param bool     $get_from_post Whether to get the value from the $_POST array as opposed to $field_values
	 *
	 * @return array|mixed|string
	 */
	public static function get_field_value( &$field, $field_values = array(), $get_from_post = true ) {

		if ( ! $field instanceof GF_Field ) {
			$field = GF_Fields::create( $field );
		}

		if ( $field->type == 'post_category' ) {
			$field = GFCommon::add_categories_as_choices( $field, '' );
		}

		$value = $field->get_value_submission( $field_values, $get_from_post );

		if ( $field->get_input_type() == 'list' && $field->enableColumns && $get_from_post && rgpost( 'is_submit_' . $field->formId ) ) {
			/** @var GF_Field_List $field */
			$value = $field->create_list_array_recursive( $value );
		}

		return $value;
	}

	/**
	 * @deprecated 2.4
	 * @remove-in 3.0
	 *
	 * @param int $expiration_days
	 *
	 * @return false|int
	 */
	public static function purge_expired_incomplete_submissions( $expiration_days = 30 ) {
		_deprecated_function( 'GFFormsModel::purge_expired_incomplete_submissions', '2.4', 'GFFormsModel::purge_expired_draft_submissions' );
		return self::purge_expired_draft_submissions( $expiration_days = 30 );
	}

	/**
	 * Purges expired draft submissions.
	 *
	 * @since 2.4
	 *
	 * @param int $expiration_days
	 *
	 * @return false|int
	 */
	public static function purge_expired_draft_submissions( $expiration_days = 30 ) {
		global $wpdb;

		/**
		 * Overrides the number of days until draft submissions are purged.
		 *
		 * @since 1.9
		 *
		 * @param int $expiration_days The number of days until expiration. Defaults to 30.
		 */
		$expiration_days = apply_filters( 'gform_incomplete_submissions_expiration_days', $expiration_days );
		$expiration_date = gmdate( 'Y-m-d H:i:s', time() - ( $expiration_days * 24 * 60 * 60 ) );

		$table  = version_compare( self::get_database_version(), '2.3-dev-1', '<' ) ? self::get_incomplete_submissions_table_name() : self::get_draft_submissions_table_name();

		$query = array(
			'delete' => 'DELETE',
			'from'   => sprintf( 'FROM %s', $table ),
			'where'  => $wpdb->prepare( 'WHERE date_created < %s', $expiration_date ),
		);

		/**
		 * Allows the query used to purge expired draft (save and continue) submissions to be overridden.
		 *
		 * @since 2.1.1.20
		 *
		 * @param array $query The delete, from, and where arguments to be used when the query is performed.
		 */
		$query = apply_filters( 'gform_purge_expired_incomplete_submissions_query', $query );

		$result = $wpdb->query( implode( "\n", $query ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		return $result;
	}

	/**
	 *
	 * @deprecated 2.4
	 * @remove-in 3.0
	 *
	 * @param $token
	 *
	 * @return false|int
	 */
	public static function delete_incomplete_submission( $token ) {
		_deprecated_function( 'GFFormsModel::delete_incomplete_submission', '2.4', 'GFFormsModel::delete_draft_submission' );
		return self::delete_draft_submission( $token );
	}

	/**
	 * Deletes a draft submission.
	 *
	 * @since 2.4
	 *
	 * @param $token
	 *
	 * @return false|int
	 */
	public static function delete_draft_submission( $token ) {
		global $wpdb;

		$table  = version_compare( self::get_database_version(), '2.3-dev-1', '<' ) ? self::get_incomplete_submissions_table_name() : self::get_draft_submissions_table_name();
		$result = $wpdb->query( $wpdb->prepare( "DELETE FROM %i WHERE uuid = %s", $table, $token ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

		return $result;
	}

	/**
	 *
	 * @deprecated 2.4
	 * @remove-in 3.0
	 *
	 * @param        $form
	 * @param        $entry
	 * @param        $field_values
	 * @param        $page_number
	 * @param        $files
	 * @param        $form_unique_id
	 * @param        $ip
	 * @param        $source_url
	 * @param string $resume_token
	 *
	 * @return bool|false|int|string
	 */
	public static function save_incomplete_submission( $form, $entry, $field_values, $page_number, $files, $form_unique_id, $ip, $source_url, $resume_token = '' ) {
		_deprecated_function( 'GFFormsModel::save_incomplete_submission', '2.4', 'GFFormsModel::save_draft_submission' );
		return self::save_draft_submission( $form, $entry, $field_values, $page_number, $files, $form_unique_id, $ip, $source_url, $resume_token );
	}

	/**
	 * Saves the draft submission.
	 *
	 * @since 2.4
	 *
	 * @param        $form
	 * @param        $entry
	 * @param        $field_values
	 * @param        $page_number
	 * @param        $files
	 * @param        $form_unique_id
	 * @param        $ip
	 * @param        $source_url
	 * @param string $resume_token
	 *
	 * @return bool|false|int|string
	 */
	public static function save_draft_submission( $form, $entry, $field_values, $page_number, $files, $form_unique_id, $ip, $source_url, $resume_token = '' ) {
		if ( ! is_array( $form['fields'] ) ) {
			return;
		}
		global $wpdb;

		$table  = version_compare( self::get_database_version(), '2.3-dev-1', '<' ) ? self::get_incomplete_submissions_table_name() : self::get_draft_submissions_table_name();

		$submitted_values = array();
		foreach ( $form['fields'] as $field ) {
			/* @var GF_Field $field */
			if ( $field->is_payment ) {
				continue;
			}

			$submitted_values[ $field->id ] = RGFormsModel::get_field_value( $field, $field_values );
		}

		/**
		 * Allows the modification of submitted values before the draft submission is saved.
		 *
		 * @since 1.9
		 *
		 * @param array $submitted_values The submitted values
		 * @param array $form             The Form object
		 */
		$submitted_values = apply_filters( 'gform_submission_values_pre_save', $submitted_values, $form );

		$submission['submitted_values'] = $submitted_values;
		$submission['partial_entry']    = $entry;
		$submission['field_values']     = $field_values;
		$submission['page_number']      = $page_number;
		$submission['files']            = $files;
		$submission['gform_unique_id']  = $form_unique_id;

		// Issue a new token if no longer valid
		if ( ! empty( $resume_token ) ) {
			$sql = $wpdb->prepare( "SELECT COUNT(*) FROM %i WHERE uuid = %s", $table, $resume_token );
			$count = $wpdb->get_var( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			if ( $count != 1 ) {
				$resume_token = false;
			}
		}

		$is_new = empty( $resume_token );

		if ( $is_new ) {
			$resume_token = self::get_uuid();
		}

		$submission_json = json_encode( $submission );

		$submission_json = self::filter_draft_submission_pre_save( $submission_json, $resume_token, $form );

		if ( $is_new ) {
			$result = $wpdb->insert( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
				$table,
				array(
					'uuid'         => $resume_token,
					'form_id'      => $form['id'],
					'date_created' => current_time( 'mysql', true ),
					'submission'   => $submission_json,
					'ip'           => $ip,
					'source_url'   => $source_url,
				),
				array(
					'%s',
					'%d',
					'%s',
					'%s',
					'%s',
					'%s',
				)
			);
		} else {
			$result = $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
				$table,
				array(
					'form_id'      => $form['id'],
					'date_created' => current_time( 'mysql', true ),
					'submission'   => $submission_json,
					'ip'           => $ip,
					'source_url'   => $source_url,
				),
				array( 'uuid' => $resume_token ),
				array(
					'%d',
					'%s',
					'%s',
					'%s',
					'%s',
				),
				array( '%s' )
			);
		}

		/**
		 * Fires after an draft submission is saved
		 *
		 * @since 1.9
		 *
		 * @param array  $submission   Contains the partially submitted entry, fields, values, and files.
		 * @param string $resume_token The unique resume token that was generated for this partial submission
		 * @param array  $form         The Form object
		 * @param array  $entry        The Entry object
		 */
		do_action( 'gform_incomplete_submission_post_save', $submission, $resume_token, $form, $entry );

		return $result ? $resume_token : $result;
	}

	/**
	 * Filters the submission json string before saving.
	 *
	 * @since 2.4
	 *
	 * @param $submission_json
	 * @param $resume_token
	 * @param $form
	 *
	 * @return string
	 */
	private static function filter_draft_submission_pre_save( $submission_json, $resume_token, $form ) {
		/**
		 * Allows the draft submission to be overridden before it is saved to the database.
		 *
		 * @since 2.3.3.1
		 *
		 * @param string $submission_json {
		 *    JSON encoded associative array containing this incomplete submission.
		 *
		 *    @type array      $submitted_values The submitted values.
		 *    @type array      $partial_entry    The draft entry created from the submitted values.
		 *    @type null|array $field_values     The dynamic population field values.
		 *    @type int        $page_number      The forms current page number.
		 *    @type array      $files            The uploaded file properties.
		 *    @type string     $gform_unique_id  The unique id for this submission.
		 * }
		 * @param string $resume_token The unique token which can be used to resume this incomplete submission at a later date/time.
		 * @param array  $form         The form which this incomplete submission was created for.
		 */
		$submission_json = apply_filters( 'gform_incomplete_submission_pre_save', $submission_json, $resume_token, $form );

		return $submission_json;
	}

	/**
	 * Updates a draft submission.
	 *
	 * @since 2.4
	 *
	 * @param string $resume_token The uuid of the draft submission to be updated.
	 * @param array  $form
	 * @param string $date_created
	 * @param string $ip
	 * @param string $source_url
	 * @param string $submission_json
	 *
	 * @return bool|false|int|string
	 */
	public static function update_draft_submission( $resume_token, $form, $date_created, $ip, $source_url, $submission_json ) {
		global $wpdb;

		$form_id = $form['id'];

		$submission_json = self::filter_draft_submission_pre_save( $submission_json, $resume_token, $form );

		$table = version_compare( self::get_database_version(), '2.3-dev-1', '<' ) ? self::get_incomplete_submissions_table_name() : self::get_draft_submissions_table_name();

		$result = $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			$table,
			array(
				'form_id'      => $form_id,
				'date_created' => $date_created,
				'submission'   => $submission_json,
				'ip'           => $ip,
				'source_url'   => $source_url,
			),
			array( 'uuid' => $resume_token ),
			array(
				'%d',
				'%s',
				'%s',
				'%s',
				'%s',
			),
			array( '%s' )
		);

		return $result ? $resume_token : $result;
	}

	/**
	 * Returns a UUID. Uses openssl_random_pseudo_bytes() if available and falls back to mt_rand().
	 *
	 * source: http://stackoverflow.com/questions/2040240/php-function-to-generate-v4-uuid
	 *
	 * @param string $s The separator e.g. '-'
	 *
	 * @return string
	 */
	public static function get_uuid( $s = '' ) {

		if ( function_exists( 'openssl_random_pseudo_bytes' ) ) { // PHP 5 >= 5.3.0
			$data = openssl_random_pseudo_bytes( 16 );

			$data[6] = chr( ord( $data[6] ) & 0x0f | 0x40 ); // set version to 0100
			$data[8] = chr( ord( $data[8] ) & 0x3f | 0x80 ); // set bits 6-7 to 10

			return vsprintf( "%s%s{$s}%s{$s}%s{$s}%s{$s}%s%s%s", str_split( bin2hex( $data ), 4 ) );
		} else {
			return sprintf(
				"%04x%04x{$s}%04x{$s}%04x{$s}%04x{$s}%04x%04x%04x",
				// 32 bits for "time_low"
				mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ),

				// 16 bits for 'time_mid'
				mt_rand( 0, 0xffff ),

				// 16 bits for 'time_hi_and_version',
				// four most significant bits holds version number 4
				mt_rand( 0, 0x0fff ) | 0x4000,

				// 16 bits, 8 bits for 'clk_seq_hi_res',
				// 8 bits for 'clk_seq_low',
				// two most significant bits holds zero and one for variant DCE1.1
				mt_rand( 0, 0x3fff ) | 0x8000,

				// 48 bits for 'node'
				mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff )
			);
		}

	}

	/**
	 * @deprecated 2.4
	 * @remove-in 3.0
	 *
	 * @param $resume_token
	 *
	 * @return array|null|object
	 */
	public static function get_incomplete_submission_values( $resume_token ) {
		_deprecated_function( 'GFFormsModel::get_incomplete_submission_values', '2.4', 'GFFormsModel::get_draft_submission_values' );
		return self::get_draft_submission_values( $resume_token );
	}

	/**
	 * Returns the values for the draft submission.
	 *
	 * @since 2.4
	 *
	 * @param $resume_token
	 *
	 * @return array|null|object
	 */
	public static function get_draft_submission_values( $resume_token ) {
		global $wpdb;

		self::purge_expired_draft_submissions();

		$table = version_compare( self::get_database_version(), '2.3-dev-1', '<' ) ? self::get_incomplete_submissions_table_name() : self::get_draft_submissions_table_name();
		$sql   = $wpdb->prepare( "SELECT date_created, form_id, submission, source_url FROM %i WHERE uuid = %s", $table, $resume_token );
		$row   = $wpdb->get_row( $sql, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

		if ( ! empty( $row ) ) {
			$form = self::get_form_meta( $row['form_id'] );
			$row['submission'] = self::filter_draft_submission_post_get( $row['submission'], $resume_token, $form );
		}

		return $row;
	}

	/**
	 * Filters the draft submission after reading it from the database.
	 *
	 * @since 2.4
	 *
	 * @param $submission_json
	 * @param $resume_token
	 * @param $form
	 *
	 * @return string
	 */
	private static function filter_draft_submission_post_get( $submission_json, $resume_token, $form ) {

		/**
		 * Allows the draft submission to be overridden after it is retrieved from the database but before it used to populate the form.
		 *
		 * @since 2.3.3.1
		 *
		 * @param string $submission_json {
		 *    JSON encoded associative array containing the draft submission being resumed.
		 *
		 *    @type array      $submitted_values The submitted values.
		 *    @type array      $partial_entry    The draft entry created from the submitted values.
		 *    @type null|array $field_values     The dynamic population field values.
		 *    @type int        $page_number      The forms current page number.
		 *    @type array      $files            The uploaded file properties.
		 *    @type string     $gform_unique_id  The unique id for this submission.
		 * }
		 * @param string $resume_token The unique token which was used to resume this incomplete submission.
		 * @param array  $form         The form which this incomplete submission was created for.
		 */
		$submission_json = apply_filters( 'gform_incomplete_submission_post_get', $submission_json, $resume_token, $form );
		return $submission_json;
	}

	/**
	 *
	 * @deprecated 2.4
	 * @remove-in 3.0
	 *
	 * @param $token
	 * @param $email
	 *
	 * @return false|int
	 */
	public static function add_email_to_incomplete_sumbmission( $token, $email ) {
		_deprecated_function( 'GFFormsModel::add_email_to_incomplete_sumbmission', '2.4', 'GFFormsModel::add_email_to_draft_sumbmission' );
		return self::add_email_to_draft_sumbmission( $token, $email );
	}

	/**
	 * Adds the email address to the draft submission.
	 *
	 * @since 2.4
	 *
	 * @param $token
	 * @param $email
	 *
	 * @return false|int
	 */
	public static function add_email_to_draft_sumbmission( $token, $email ) {
		global $wpdb;
		self::purge_expired_draft_submissions();

		$table  = version_compare( self::get_database_version(), '2.3-dev-1', '<' ) ? self::get_incomplete_submissions_table_name() : self::get_draft_submissions_table_name();
		$sql    = $wpdb->prepare( "UPDATE %i SET email = %s WHERE uuid = %s", $table, $email, $token );
		$result = $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

		return $result;
	}

	/**
	 * Returns the given value with leading and trailing whitespace removed if the value is not null or blank and trimming is enabled.
	 *
	 * @since unknown
	 *
	 * @param string|array|null $value   The field value to be trimmed.
	 * @param int               $form_id The ID of the form to which the field belongs.
	 * @param GF_Field          $field   The field object the value belongs to.
	 *
	 * @return string|array|null
	 */
	public static function maybe_trim_input( $value, $form_id, $field ) {

		if ( is_null( $value ) || rgblank( $value ) ) {
			return $value;
		}

		$trim_value = apply_filters( 'gform_trim_input_value', true, $form_id, $field );

		if ( $trim_value ) {
			$value = is_array( $value ) ? GFCommon::trim_deep( $value ) : trim( $value );
		}

		return $value;
	}

	public static function get_parameter_value( $name, $field_values, $field ) {
		$value = stripslashes_deep( rgget( $name ) );
		if ( rgblank( $value ) ) {
			$value = rgget( $name, $field_values );
		}

		// Converting list format
		if ( ! empty( $value ) && RGFormsModel::get_input_type( $field ) == 'list' ) {

			// Transforms this: col1|col2,col1b|col2b into this: col1,col2,col1b,col2b
			$column_count = is_array( $field->choices ) ? count( $field->choices ) : 0;

			$rows = is_array( $value ) ? $value : explode( ',', $value );

			if ( ! empty( $rows ) ) {
				$ary_rows = array();

				foreach ( $rows as $row ) {
					/**
					 * Allow modification of the delimiter used to parse List field URL parameters.
					 *
					 * @since 2.0.0
					 *
					 * @param string $delimiter    Defaults to '|';
					 * @param array  $field        GF_Field object for the current field.
					 * @param string $name         Name of the current dynamic population parameter.
					 * @param array  $field_values Array of values provided for pre-population into the form.
					 */
					$delimiter = apply_filters( 'gform_list_field_parameter_delimiter', '|', $field, $name, $field_values );
					$ary_rows  = array_merge( $ary_rows, rgexplode( $delimiter, $row, $column_count ) );
				}

				$value = $ary_rows;
			}
		}

		return gf_apply_filters( array( 'gform_field_value', $name ), $value, $field, $name );
	}

	public static function get_default_value( $field, $input_id ) {
		if ( ! is_array( $field->choices ) ) {
			// if entry is saved in separate inputs get requsted input's default value ($input_id = 2.1)
			// some fields (like Date, Time) do not save their values in separate inputs and are correctly filtered out by this condition ($input_id = 2)
			// other fields (like Email w/ Confirm-enabled) also do not save their values in separate inputs but *should be* processed as input-specific submissions ($input_id = 2)
			if ( is_array( $field->get_entry_inputs() ) || ( $field->get_input_type() == 'email' && is_array( $field->inputs ) ) ) {
				$input = RGFormsModel::get_input( $field, $input_id );
				return rgar( $input, 'defaultValue' );
			} else {
				$value = $field->get_value_default();
				if( ! IS_ADMIN ) {
					if( is_array( $value ) ) {
						foreach( $value as &$_value ) {
							$_value = GFCommon::replace_variables_prepopulate( $_value );
						}
					} else {
						$value = GFCommon::replace_variables_prepopulate( $value );
					}
				}
				return $value;
			}
		} else if ( $field->type == 'checkbox' ) {
			for ( $i = 0, $count = sizeof( $field->inputs ); $i < $count; $i ++ ) {
				$input  = $field->inputs[ $i ];
				$choice = $field->choices[ $i ];
				if ( $input['id'] == $input_id && rgar( $choice, 'isSelected' ) ) {
					return $choice['value'];
				}
			}

			return '';
		} else {
			foreach ( $field->choices as $choice ) {
				if ( rgar( $choice, 'isSelected' ) || $field->type == 'post_category' ) {
					return $choice['value'];
				}
			}

			return '';
		}

	}

	/**
	 * @param GF_Field $field
	 *
	 * @return string
	 */
	public static function get_input_type( $field ) {
		// TODO: Deprecate
		if ( ! $field instanceof GF_Field ) {
			return empty( $field['inputType'] ) ? $field['type'] : $field['inputType'];
		}

		return $field->get_input_type();
	}

	private static function get_post_field_value( $field, $lead ) {

		if ( is_array( $field->get_entry_inputs() ) ) {
			$value = array();
			foreach ( $field->inputs as $input ) {
				$val = isset( $lead[ strval( $input['id'] ) ] ) ? $lead[ strval( $input['id'] ) ] : '';
				if ( ! empty( $val ) ) {

					// replace commas in individual values to prevent individual value from being split into multiple values (checkboxes, multiselects)
					if ( $field->get_input_type() === 'checkbox' ) {
						$val = str_replace( ',', '&#44;', $val );
					}

					$value[] = $val;
				}
			}
			$value = implode( ',', $value );
		} else {
			$value = isset( $lead[ $field->id ] ) ? $lead[ $field->id ] : '';

			if ( ! empty( $value ) && $field->get_input_type() === 'multiselect' ) {
				$items = $field->to_array( $value );

				foreach ( $items as &$item ) {
					$item = str_replace( ',', '&#44;', $item );
				}

				$value = implode( ',', $items );
			}
		}

		return $value;
	}

	private static function get_post_fields( $form, $lead ) {

		$post_data                       = array();
		$post_data['post_custom_fields'] = array();
		$post_data['tags_input']         = array();
		$categories                      = array();
		$images                          = array();

		foreach ( $form['fields'] as $field ) {
			if ( self::is_field_hidden( $form, $field, array(), $lead ) ) {
				continue;
			}

			if ( $field->type == 'post_category' ) {
				$field = GFCommon::add_categories_as_choices( $field, '' );
			}

			$value = self::get_post_field_value( $field, $lead );

			switch ( $field->type ) {
				case 'post_title' :
				case 'post_excerpt' :
				case 'post_content' :
					// Prevent shortcodes from being parsed.
					$post_data[ $field->type ] = GFCommon::encode_shortcodes( $value );
					break;

				case 'post_tags' :
					$tags = explode( ',', $value );
					if ( is_array( $tags ) && sizeof( $tags ) > 0 ) {
						$post_data['tags_input'] = array_merge( $post_data['tags_input'], $tags );
					}
					break;

				case 'post_custom_field' :

					if ( $field instanceof GF_Field_FileUpload ) {
						$value = $field->to_array( $value );
						if ( ! $field->multipleFiles ) {
							$value = rgar( $value, 0 );
						}
					}

					$meta_name = $field->postCustomFieldName;

					if ( ! isset( $post_data['post_custom_fields'][ $meta_name ] ) ) {
						$post_data['post_custom_fields'][ $meta_name ] = $value;
					} else if ( ! is_array( $post_data['post_custom_fields'][ $meta_name ] ) ) {
						$post_data['post_custom_fields'][ $meta_name ] = array( $post_data['post_custom_fields'][ $meta_name ], $value );
					} else {
						$post_data['post_custom_fields'][ $meta_name ][] = $value;
					}

					break;

				case 'post_category' :
					foreach ( explode( ',', $value ) as $cat_string ) {
						$cat_array = explode( ':', $cat_string );
						// the category id is the last item in the array, access it using end() in case the category name includes colons.
						array_push( $categories, end( $cat_array ) );
					}
					break;

				case 'post_image' :
					$ary         = ! empty( $value ) ? explode( '|:|', $value ) : array();
					$url         = count( $ary ) > 0 ? $ary[0] : '';
					$title       = count( $ary ) > 1 ? $ary[1] : '';
					$caption     = count( $ary ) > 2 ? $ary[2] : '';
					$description = count( $ary ) > 3 ? $ary[3] : '';
					$alt         = count( $ary ) > 4 ? $ary[4] : '';

					array_push( $images, array( 'field_id' => $field->id, 'url' => $url, 'title' => $title, 'description' => $description, 'caption' => $caption, 'alt' => $alt ) );
					break;
			}
		}

		$post_data['post_status']   = rgar( $form, 'postStatus' );
		$post_data['post_category'] = ! empty( $categories ) ? $categories : array( rgar( $form, 'postCategory' ) );
		$post_data['images']        = $images;

		//setting current user as author depending on settings
		$post_data['post_author'] = $form['useCurrentUserAsAuthor'] && ! empty( $lead['created_by'] ) ? $lead['created_by'] : rgar( $form, 'postAuthor' );

		return $post_data;
	}

	/**
	 * Retrieves the custom field names (meta keys) for the custom field select in the form editor.
	 *
	 * @since unknown
	 * @since 2.9.10 Added the $limit parameter.
	 *
	 * @param string $limit Optional. Limits the number of custom field names returned. Default is empty (no limit).
	 *
	 * @return array
	 */
	public static function get_custom_field_names( $limit = '' ) {
		$form_id = absint( rgget( 'id' ) );

		/**
		 * Allow the postmeta query which retrieves the custom field names (meta keys) to be disabled.
		 *
		 * @since 2.3.4.1
		 *
		 * @param bool $disable_query Indicates if the custom field names query should be disabled. Default is false.
		 */
		$disable_query = gf_apply_filters( array( 'gform_disable_custom_field_names_query', $form_id ), false );

		if ( $disable_query ) {
			return array();
		}

		global $wpdb;
		$sql = "SELECT DISTINCT meta_key
			FROM $wpdb->postmeta
			WHERE meta_key NOT BETWEEN '_' AND '_z'
			HAVING meta_key NOT LIKE %s
			ORDER BY meta_key";
		if ( ! empty( $limit ) && is_numeric( $limit ) ) {
			$sql .= $wpdb->prepare( " LIMIT %d", intval( $limit ) );
		}
		$keys = $wpdb->get_col( $wpdb->prepare( $sql, $wpdb->esc_like( '_' ) . '%' ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

		return $keys;
	}

	public static function get_input_masks() {

		$masks = array(
			'US Phone'       => '(999) 999-9999',
			'US Phone + Ext' => '(999) 999-9999? x99999',
			'Date'           => '99/99/9999',
			'Tax ID'         => '99-9999999',
			'SSN'            => '999-99-9999',
			'Zip Code'       => '99999',
			'Full Zip Code'  => '99999?-9999',
		);

		return apply_filters( 'gform_input_masks', $masks );
	}

	private static function get_default_post_title() {
		global $wpdb;
		$title = 'Untitled';
		$count = 1;

		$titles = $wpdb->get_col( "SELECT post_title FROM $wpdb->posts WHERE post_title like '%Untitled%'" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$titles = array_values( $titles );
		while ( in_array( $title, $titles ) ) {
			$title = "Untitled_$count";
			$count ++;
		}

		return $title;
	}

	public static function prepare_date( $date_format, $value ) {
		$format    = empty( $date_format ) ? 'mdy' : $date_format;
		$date_info = GFCommon::parse_date( $value, $format );
		if ( ! empty( $date_info ) && ! GFCommon::is_empty_array( $date_info ) ) {
			$value = sprintf( '%s-%02d-%02d', $date_info['year'], $date_info['month'], $date_info['day'] );
		} else {
			$value = '';
		}

		return $value;
	}

	/**
	 * Prepare the value before saving it to the lead. For multi-input fields this will be called for each input.
	 *
	 * @param mixed    $form
	 * @param GF_Field $field
	 * @param mixed    $value
	 * @param mixed    $input_name
	 * @param mixed    $lead_id the current lead ID, used for fields that are processed after other fields have been saved (ie Total, Calculations)
	 * @param mixed    $lead    passed by the RGFormsModel::create_lead() method, lead ID is not available for leads created by this function
	 *
	 * @return mixed
	 */
	public static function prepare_value( $form, $field, $value, $input_name, $lead_id, $lead = array() ) {

		$value = $field->get_value_save_entry( $value, $form, $input_name, $lead_id, $lead );


		// special format for Post Category fields
		if ( $field->type == 'post_category' ) {
			$is_multiselect = $field->inputType === 'multiselect';
			$full_values    = array();

			if ( ! is_array( $value ) ) {
				$value = $is_multiselect ? $field->to_array( $value ) : explode( ',', $value );
			}

			foreach ( $value as $cat_id ) {
				$cat           = get_term( $cat_id, 'category' );
				$full_values[] = ! is_wp_error( $cat ) && is_object( $cat ) ? $cat->name . ':' . $cat_id : '';
			}

			$value = $is_multiselect ? $field->to_string( $full_values ) : implode( ',', $full_values );
		}

		//do not save price fields with blank price
		if ( $field->enablePrice ) {
			list( $label, $price ) = rgexplode( '|', $value, 2, true );

			$is_empty = ( strlen( trim( $price ) ) <= 0 );
			if ( $is_empty ) {
				$value = '';
			}
		}

		return $value;
	}

	public static function is_checkbox_checked( $field_id, $field_label, $lead, $form ) {

		//looping through lead detail values trying to find an item identical to the column label. Mark with a tick if found.
		$lead_field_keys = array_keys( $lead );
		foreach ( $lead_field_keys as $input_id ) {
			//mark as a tick if input label (from form meta) is equal to submitted value (from lead)
			if ( is_numeric( $input_id ) && absint( $input_id ) == absint( $field_id ) ) {
				if ( $lead[ $input_id ] == $field_label ) {
					return $lead[ $input_id ];
				} else {
					$field = RGFormsModel::get_field( $form, $field_id );
					if ( $field->enableChoiceValue || $field->enablePrice ) {
						foreach ( $field->choices as $choice ) {
							if ( $choice['value'] == $lead[ $field_id ] ) {
								return $choice['value'];
							} else if ( $field->enablePrice ) {
								list( $val, $price ) = rgexplode( '|', $lead[ $field_id ], 2, true );

								if ( $val == $choice['value'] ) {
									return $choice['value'];
								}
							}
						}
					}
				}
			}
		}

		return false;
	}

	/**
	 * @depecated 1.9
	 * @remove-in 3.0
	 */
	public static function get_fileupload_value( $form_id, $input_name ) {
		_deprecated_function( 'GFFormsModel::get_fileupload_value', '1.9', 'GF_Field_Fileupload::get_fileupload_value' );
		global $_gf_uploaded_files;

		GFCommon::log_debug( 'GFFormsModel::get_fileupload_value(): Starting.' );

		if ( empty( $_gf_uploaded_files ) ) {
			GFCommon::log_debug( 'GFFormsModel::get_fileupload_value(): No files uploaded. Exiting.' );
			$_gf_uploaded_files = array();
		}


		if ( ! isset( $_gf_uploaded_files[ $input_name ] ) ) {

			//check if file has already been uploaded by previous step
			$file_info     = self::get_temp_filename( $form_id, $input_name );
			$tmp_location  = GFFormsModel::get_tmp_upload_location( $form_id );
			$temp_filepath = $tmp_location['path'] . $file_info['temp_filename'];
			GFCommon::log_debug( 'GFFormsModel::get_fileupload_value(): Temp file path: ' . $temp_filepath );
			if ( $file_info && file_exists( $temp_filepath ) ) {
				GFCommon::log_debug( 'GFFormsModel::get_fileupload_value(): Moving temp file: ' . $temp_filepath );
				$_gf_uploaded_files[ $input_name ] = self::move_temp_file( $form_id, $file_info );
			} else if ( ! empty( $_FILES[ $input_name ]['name'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
				GFCommon::log_debug( 'GFFormsModel::get_fileupload_value(): Uploading file: ' . $_FILES[ $input_name ]['name'] ); // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
				$_gf_uploaded_files[ $input_name ] = self::upload_file( $form_id, $_FILES[ $input_name ] ); // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
			}
		}

		return rgget( $input_name, $_gf_uploaded_files );
	}

	public static function get_form_unique_id( $form_id ) {
		$unique_id = '';
		if ( rgpost( 'gform_submit' ) == $form_id ) {
			$posted_uid = rgpost( 'gform_unique_id' );
			if ( false === empty( $posted_uid ) && ctype_alnum( $posted_uid )) {
				$unique_id = $posted_uid;
				self::$unique_ids[ $form_id ] = $unique_id;
			} elseif ( isset( self::$unique_ids[ $form_id ] ) ) {
				$unique_id = self::$unique_ids[ $form_id ];
			} else {
				$unique_id = uniqid();
				self::$unique_ids[ $form_id ] = $unique_id;
			}
		}

		return $unique_id;
	}

	/**
	 * @depecated 2.9.18
	 * @remove-in 4.0
	 */
	public static function get_temp_filename( $form_id, $input_name ) {
		_deprecated_function( __METHOD__, '2.9.18', '$file_upload_field->get_tmp_file_details( $file_or_name )' );

		$uploaded_filename = ! empty( $_FILES[ $input_name ]['name'] ) && $_FILES[ $input_name ]['error'] === 0 ? $_FILES[ $input_name ]['name'] : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized

		if ( empty( $uploaded_filename ) && isset( self::$uploaded_files[ $form_id ] ) ) {
			$uploaded_filename = rgget( $input_name, self::$uploaded_files[ $form_id ] );
		}

		if ( empty( $uploaded_filename ) ) {
			return false;
		}

		$ext_check      = is_array( $uploaded_filename ) ? $uploaded_filename[0]['uploaded_filename'] : $uploaded_filename;
		$form_unique_id = self::get_form_unique_id( $form_id );
		$extension      = pathinfo( $ext_check, PATHINFO_EXTENSION );
		$temp_filename  = "{$form_unique_id}_{$input_name}.{$extension}";

		GFCommon::log_debug( __METHOD__ . '(): Uploaded filename is ' . $ext_check . ' and temporary filename is ' . $temp_filename );
		return array( 'uploaded_filename' => $uploaded_filename, 'temp_filename' => $temp_filename );

	}

	public static function get_choice_text( $field, $value, $input_id = 0 ) {
		if ( ! is_array( $field->choices ) ) {
			return $value;
		}

		foreach ( $field->choices as $choice ) {
			if ( is_array( $value ) && self::choice_value_match( $field, $choice, $value[ $input_id ] ) ) {
				return $choice['text'];
			} else if ( ! is_array( $value ) && self::choice_value_match( $field, $choice, $value ) ) {
				return $choice['text'];
			}
		}

		return is_array( $value ) ? '' : $value;
	}


	public static function choice_value_match( $field, $choice, $value ) {
		$choice_value = GFFormsModel::maybe_trim_input( $choice['value'], $field->formId, $field );
		$value        = GFFormsModel::maybe_trim_input( $value, $field->formId, $field );

		$allowed_html    = wp_kses_allowed_html( 'post' );
		$sanitized_value = wp_kses( $value, $allowed_html );

		if ( $choice_value == $value || $choice_value == $sanitized_value ) {
			return true;
		} else if ( $field->enablePrice ) {
			list( $val, $price ) = rgexplode( '|', $value, 2, true );

			$sanitized_val = wp_kses( $val, $allowed_html );

			if ( $choice['value'] == $val || $choice['value'] == $sanitized_val ) {
				return true;
			}
		} // add support for prepopulating multiselects @alex
		else if ( RGFormsModel::get_input_type( $field ) == 'multiselect' ) {
			$values           = $field->to_array( $value );
			$sanitized_values = $field->to_array( $sanitized_value );

			if ( in_array( $choice_value, $values ) || in_array( $choice_value, $sanitized_values ) ) {
				return true;
			}
		}

		return false;
	}

	public static function choices_value_match( $field, $choices, $value ) {
		foreach ( $choices as $choice ) {
			if ( self::choice_value_match( $field, $choice, $value ) ) {
				return true;
			}
		}

		return false;
	}

	public static function create_post( $form, &$lead ) {
		GFCommon::timer_start( __METHOD__ );
		GFCommon::log_debug( 'GFFormsModel::create_post(): Starting.' );

		$has_post_field = (bool) GFCache::get( 'has_post_field_entry_' . rgar( $lead, 'id' ), $found, false );
		if ( ! $found ) {
			// Fallback for when post creation is triggered at a later date/time.
			foreach ( $form['fields'] as $field ) {
				if ( GFCommon::is_post_field( $field ) && ! self::is_field_hidden( $form, $field, array(), $lead ) ) {
					$has_post_field = true;
					break;
				}
			}
		}

		//if this form does not have any post fields, don't create a post
		if ( ! $has_post_field ) {
			GFCommon::log_debug( "GFFormsModel::create_post(): Stopping. The form doesn't have any post fields." );

			return $lead;
		}

		//processing post fields
		GFCommon::log_debug( 'GFFormsModel::create_post(): Getting post fields.' );
		$post_data = self::get_post_fields( $form, $lead );

		//allowing users to change post fields before post gets created
		$post_data = gf_apply_filters( array( 'gform_post_data', $form['id'] ), $post_data, $form, $lead );

		//adding default title if none of the required post fields are in the form (will make sure wp_insert_post() inserts the post)
		if ( empty( $post_data['post_title'] ) && empty( $post_data['post_content'] ) && empty( $post_data['post_excerpt'] ) ) {
			$post_data['post_title'] = self::get_default_post_title();
		}

		// remove original post status and save it for later
		$post_status = $post_data['post_status'];

		// replace original post status with 'draft' so other plugins know this post is not fully populated yet
		$post_data['post_status'] = 'draft';

		// inserting post
		GFCommon::log_debug( 'GFFormsModel::create_post(): Inserting post via wp_insert_post().' );
		$post_id = wp_insert_post( $post_data, true );
		GFCommon::log_debug( 'GFFormsModel::create_post(): Result from wp_insert_post(): ' . print_r( $post_id, 1 ) );

		if ( is_wp_error( $post_id ) ) {
			GFCommon::log_debug( __METHOD__ . '(): $post_data => ' . print_r( $post_data, 1 ) );

			return false;
		}

		// Add the post id to the entry so it is available during merge tag replacement.
		$lead['post_id'] = $post_id;

		//adding form id and entry id hidden custom fields
		add_post_meta( $post_id, '_gform-form-id', $form['id'] );
		add_post_meta( $post_id, '_gform-entry-id', $lead['id'] );

		$post_images = array();
		if ( ! empty( $post_data['images'] ) ) {
			// Creating post images.
			GFCommon::log_debug( 'GFFormsModel::create_post(): Processing post images.' );

			foreach ( $post_data['images'] as $image ) {
				if ( empty( $image['url'] ) ) {
					GFCommon::log_debug( __METHOD__ . '(): No image to process for field #' . $image['field_id'] );
					continue;
				}

				$image_meta = array(
					'post_excerpt' => $image['caption'],
					'post_content' => $image['description'],
				);

				// Adding title only if it is not empty. It will default to the file name if it is not in the array.
				if ( ! empty( $image['title'] ) ) {
					$image_meta['post_title'] = $image['title'];
				}

				GFCommon::log_debug( sprintf( '%s(): Field #%s. URL: %s', __METHOD__, $image['field_id'], $image['url'] ) );
				$media_id = self::media_handle_upload( $image['url'], $post_id, $image_meta );

				if ( $media_id ) {

					// Save media id for post body/title template variable replacement (below).
					$post_images[ $image['field_id'] ] = $media_id;
					$lead[ $image['field_id'] ] .= "|:|$media_id";

					// Update the alt text.
					update_post_meta( $media_id, '_wp_attachment_image_alt', $image['alt'] );

					// Setting the featured image.
					$field = RGFormsModel::get_field( $form, $image['field_id'] );
					if ( $field->postFeaturedImage ) {
						$result = set_post_thumbnail( $post_id, $media_id );
						GFCommon::log_debug( __METHOD__ . '(): Setting the featured image. Result from set_post_thumbnail(): ' . var_export( $result, 1 ) ); // phpcs:ignore QITStandard.PHP.DebugCode.DebugFunctionFound
					}
				}
			}
		}

		//adding custom fields
		GFCommon::log_debug( 'GFFormsModel::create_post(): Adding custom fields.' );
		foreach ( $post_data['post_custom_fields'] as $meta_name => $meta_value ) {
			if ( ! is_array( $meta_value ) ) {
				$meta_value = array( $meta_value );
			}

			$meta_index = 0;
			foreach ( $meta_value as $value ) {
				GFCommon::log_debug( 'GFFormsModel::create_post(): Getting custom field: ' . $meta_name );
				$custom_field = self::get_custom_field( $form, $meta_name, $meta_index );

				//replacing template variables if template is enabled
				if ( $custom_field && $custom_field->customFieldTemplateEnabled ) {
					$value = self::process_post_template( $custom_field->customFieldTemplate, 'post_custom_field', $post_images, $post_data, $form, $lead );
				}
				switch ( RGFormsModel::get_input_type( $custom_field ) ) {
					case 'list' :
						$value = maybe_unserialize( $value );
						if ( is_array( $value ) ) {
							foreach ( $value as $item ) {
								if ( is_array( $item ) ) {
									$item = implode( '|', $item );
								}

								if ( ! rgblank( $item ) ) {
									add_post_meta( $post_id, $meta_name, $item );
								}
							}
						}
						break;

					case 'multiselect' :
					case 'checkbox' :
						$value = explode( ',', $value );
						if ( is_array( $value ) ) {
							foreach ( $value as $item ) {
								if ( ! rgblank( $item ) ) {
									// add post meta and replace HTML symbol in $item with real comma
									add_post_meta( $post_id, $meta_name, str_replace( '&#44;', ',', $item ) );
								}
							}
						}
						break;

					case 'date' :
						$value = GFCommon::date_display( $value, rgar( $custom_field, 'dateFormat' ) );
						if ( ! rgblank( $value ) ) {
							add_post_meta( $post_id, $meta_name, $value );
						}
						break;

					default :
						if ( ! rgblank( $value ) ) {
							add_post_meta( $post_id, $meta_name, $value );
						}
						break;
				}

				$meta_index ++;
			}
		}

		$has_content_field = sizeof( self::get_fields_by_type( $form, array( 'post_content' ) ) ) > 0;
		$has_title_field   = sizeof( self::get_fields_by_type( $form, array( 'post_title' ) ) ) > 0;
		$post              = false;

		//if a post field was configured with a content or title template, process template
		if ( ( rgar( $form, 'postContentTemplateEnabled' ) && $has_content_field ) || ( rgar( $form, 'postTitleTemplateEnabled' ) && $has_title_field ) ) {

			$post = get_post( $post_id );

			if ( rgar( $form, 'postContentTemplateEnabled' ) && $has_content_field ) {
				$post_content = self::process_post_template( $form['postContentTemplate'], 'post_content', $post_images, $post_data, $form, $lead );

				//updating post content
				$post->post_content = $post_content;
			}

			if ( rgar( $form, 'postTitleTemplateEnabled' ) && $has_title_field ) {
				$post_title = self::process_post_template( $form['postTitleTemplate'], 'post_title', $post_images, $post_data, $form, $lead );

				//updating post
				$post->post_title = $post_title;
				$post->post_name  = $post_title;
			}
		}

		// update post status back to original status (if not draft)
		if ( $post_status != 'draft' ) {
			$post              = is_object( $post ) ? $post : get_post( $post_id );
			$post->post_status = $post_status;
		}

		// if post has been modified since creation, save updates
		if ( is_object( $post ) ) {
			GFCommon::log_debug( 'GFFormsModel::create_post(): Updating post.' );
			wp_update_post( $post );
		}


		//adding post format
		if ( current_theme_supports( 'post-formats' ) && rgar( $form, 'postFormat' ) ) {

			$formats     = get_theme_support( 'post-formats' );
			$post_format = rgar( $form, 'postFormat' );

			if ( is_array( $formats ) ) {
				$formats = $formats[0];
				if ( in_array( $post_format, $formats ) ) {
					set_post_format( $post_id, $post_format );
				} else if ( '0' == $post_format ) {
					set_post_format( $post_id, false );
				}
			}
		}

		// Update the post_id in the database for this entry.
		GFCommon::log_debug( 'GFFormsModel::create_post(): Updating entry with post id.' );
		self::update_lead_property( $lead['id'], 'post_id', $post_id );

		$gform_after_create_post_args = array( 'gform_after_create_post', $form['id'] );
		if ( gf_has_action( $gform_after_create_post_args ) ) {
			GFCommon::log_debug( __METHOD__ . '(): Executing functions hooked to gform_after_create_post.' );
			/**
			 * Fires after a post, from a form with post fields, is created
			 *
			 * @param int   $form    ['id'] The ID of the form where the new post was created
			 * @param int   $post_id The new Post ID created after submission
			 * @param array $lead    The Lead Object
			 * @param array $form    The Form Object for the form used to create the post
			 */
			gf_do_action( $gform_after_create_post_args, $post_id, $lead, $form );
			GFCommon::log_debug( __METHOD__ . '(): Completed gform_after_create_post.' );
		}

		GFCommon::log_debug( __METHOD__ . sprintf( '(): Post creation completed in %F seconds.', GFCommon::timer_end( __METHOD__ ) ) );

		return $post_id;
	}

	/**
	 * Process any merge tags and shortcodes found in the template.
	 *
	 * @param string $template The template.
	 * @param string $field_type The field type currently being processed. Possible values: post_custom_field, post_content, or post_title.
	 * @param array $post_images The uploaded post images.
	 * @param array $post_data The post data prepared from the current entry.
	 * @param array $form The form currently being processed.
	 * @param array $entry The entry currently being processed.
	 *
	 * @return string
	 */
	public static function process_post_template( $template, $field_type, $post_images, $post_data, $form, $entry ) {
		GFCommon::log_debug( __METHOD__ . "(): Processing {$field_type} template." );

		//replacing post image variables
		$template = GFCommon::replace_variables_post_image( $template, $post_images, $entry );

		//replacing all other variables
		$template = GFCommon::replace_variables( $template, $form, $entry, false, false, false );

		if ( $field_type != 'post_content' ) {
			$process_template_shortcodes = true;

			/**
			 * Allow shortcode processing of custom field and post title templates to be disabled.
			 *
			 * @param boolean $process_template_shortcodes Should the shortcodes be processed? Default is true.
			 * @param string $field_type The field type currently being processed. Possible values: post_custom_field, post_content, or post_title.
			 * @param array $post_data The post data prepared from the current entry.
			 * @param array $form The form currently being processed.
			 * @param array $entry The entry currently being processed.
			 *
			 * @since 2.0.0.4
			 */
			$process_template_shortcodes = apply_filters( 'gform_process_template_shortcodes_pre_create_post', $process_template_shortcodes, $field_type, $post_data, $form, $entry );
			$process_template_shortcodes = apply_filters( 'gform_process_template_shortcodes_pre_create_post_' . $form['id'], $process_template_shortcodes, $field_type, $post_data, $form, $entry );


			if ( $process_template_shortcodes ) {
				$template = do_shortcode( $template );
			}
		}

		return $template;
	}

	private static function get_custom_field( $form, $meta_name, $meta_index ) {
		$custom_fields = self::get_fields_by_type( $form, array( 'post_custom_field' ) );

		$index = 0;
		foreach ( $custom_fields as $field ) {
			if ( $field->postCustomFieldName == $meta_name ) {
				if ( $meta_index == $index ) {
					return $field;
				}
				$index ++;
			}
		}

		return false;
	}

	private static function copy_post_image( $url, $post_id ) {
		$time = current_time( 'mysql' );

		if ( $post = get_post( $post_id ) ) {
			if ( substr( $post->post_date, 0, 4 ) > 0 ) {
				$time = $post->post_date;
			}
		}

		//making sure there is a valid upload folder
		if ( ! ( ( $upload_dir = wp_upload_dir( $time ) ) && false === $upload_dir['error'] ) ) {
			return false;
		}

		$form_id = get_post_meta( $post_id, '_gform-form-id', true );

		/**
		 * Filter the media upload location.
		 *
		 * @param array $upload_dir The current upload directory’s path and url.
		 * @param int $form_id The ID of the form currently being processed.
		 * @param int $post_id The ID of the post created from the entry currently being processed.
		 */
		$upload_dir = gf_apply_filters( 'gform_media_upload_path', $form_id, $upload_dir, $form_id, $post_id );

		if ( ! file_exists( $upload_dir['path'] ) ) {
			if ( ! wp_mkdir_p( $upload_dir['path'] ) ) {
				return false;
			}
		}

		$name     = wp_basename( $url );
		$filename = wp_unique_filename( $upload_dir['path'], $name );

		// the destination path
		$new_file = $upload_dir['path'] . "/$filename";

		// the source path
		$upload_root_info = GF_Field_FileUpload::get_upload_root_info( $form_id );
		if ( ! str_starts_with( $url, $upload_root_info['url'] ) ) {
			return false;
		}

		$path = str_replace( $upload_root_info['url'], $upload_root_info['path'], $url );
		if ( ! file_exists( $path ) ) {
			return false;
		}

		// copy the file to the destination path
		if ( ! copy( $path, $new_file ) ) {
			return false;
		}

		// Set correct file permissions
		$stat  = stat( dirname( $new_file ) );
		$perms = $stat['mode'] & 0000666;
		@ chmod( $new_file, $perms );

		// Compute the URL
		$url = $upload_dir['url'] . "/$filename";

		if ( is_multisite() ) {
			delete_transient( 'dirsize_cache' );
		}

		$type = wp_check_filetype( $new_file );

		return array( 'file' => $new_file, 'url' => $url, 'type' => $type['type'] );

	}

	public static function media_handle_upload( $url, $post_id, $post_data = array() ) {

		// WordPress Administration API required for the media_handle_upload() function.
		require_once( ABSPATH . 'wp-admin/includes/image.php' );
		require_once( ABSPATH . 'wp-admin/includes/media.php' );

		$name = wp_basename( $url );

		$file = self::copy_post_image( $url, $post_id );

		if ( ! $file ) {
			GFCommon::log_debug( __METHOD__ . '(): Image could not be copied to the media directory.' );

			return false;
		}

		$name_parts = pathinfo( $name );
		$name       = trim( substr( $name, 0, - ( 1 + strlen( $name_parts['extension'] ) ) ) );

		$url     = $file['url'];
		$type    = $file['type'];
		$file    = $file['file'];
		$title   = $name;
		$content = '';

		// use image exif/iptc data for title and caption defaults if possible
		if ( $image_meta = @wp_read_image_metadata( $file ) ) {
			if ( trim( $image_meta['title'] ) && ! is_numeric( sanitize_title( $image_meta['title'] ) ) ) {
				$title = $image_meta['title'];
			}
			if ( trim( $image_meta['caption'] ) ) {
				$content = $image_meta['caption'];
			}
		}

		// Construct the attachment array
		$attachment = array_merge(
			array(
				'post_mime_type' => $type,
				'guid'           => $url,
				'post_parent'    => $post_id,
				'post_title'     => $title,
				'post_content'   => $content,
			), $post_data
		);

		// Save the data
		$id = wp_insert_attachment( $attachment, $file, $post_id );
		if ( ! is_wp_error( $id ) ) {
			wp_update_attachment_metadata( $id, wp_generate_attachment_metadata( $id, $file ) );
		}

		GFCommon::log_debug( __METHOD__ . '(): Image copied to the media directory. Result from wp_insert_attachment(): ' . print_r( $id, 1 ) );

		return $id;
	}

	public static function save_input( $form, $field, &$lead, $current_fields, $input_id ) {

		if ( isset( $field->fields ) && is_array( $field->fields ) ) {
			foreach( $field->fields as $sub_field ) {
				$inputs = $sub_field->get_entry_inputs();
				if ( is_array( $inputs ) ) {
					foreach ( $inputs as $input ) {
						self::save_input( $form, $sub_field, $lead, $current_fields, $input['id'] );
					}
				} else {
					self::save_input( $form, $sub_field, $lead, $current_fields, $sub_field->id );
				}
				foreach ( $current_fields as $current_field ) {
					if ( intval( $current_field->meta_key ) == $sub_field->id && ! isset( $current_field->update ) ) {
						$current_field->delete = true;
						$result = self::queue_batch_field_operation( $form, $lead, $sub_field, $current_field->id, $current_field->meta_key, '', $current_field->item_index );
						GFCommon::log_debug( __METHOD__ . "(): Deleting: {$field->label}(#{$sub_field->id}{$current_field->item_index} - {$field->type}). Result: " . var_export( $result, 1 ) ); // phpcs:ignore QITStandard.PHP.DebugCode.DebugFunctionFound
					}
				}
			}
			return;
		}

		$input_name = 'input_' . str_replace( '.', '_', $input_id );

		if ( $field->enableCopyValuesOption && rgpost( 'input_' . $field->id . '_copy_values_activated' ) ) {
			$source_field_id   = $field->copyValuesOptionField;
			$source_input_name = str_replace( 'input_' . $field->id, 'input_' . $source_field_id, $input_name );
			$value             = rgpost( $source_input_name );
		} else {
			$value = rgpost( $input_name );
		}

		$value = self::maybe_trim_input( $value, $form['id'], $field );

		//ignore file upload when nothing was sent in the admin
		//ignore post fields in the admin
		$type     = self::get_input_type( $field );
		$is_admin = GFCommon::is_entry_detail();

		if ( $is_admin && $type === 'fileupload' && $field->is_submission_files_empty() ) {
			return;
		} elseif ( $is_admin && in_array(
			$field->type,
			array(
				'post_category',
				'post_title',
				'post_content',
				'post_excerpt',
				'post_tags',
				'post_custom_field',
				'post_image',
			)
		) ) {
			return;
		}

		if ( empty( $value ) && $field->is_administrative() && ! $is_admin ) {
			$value = self::get_default_value( $field, $input_id );
		}

		self::queue_save_input_value( $value, $form, $field, $lead, $current_fields, $input_id );
	}

	/**
	 * Queues the input value for saving.
	 *
	 * @since 2.4
	 *
	 * @param string|array $value
	 * @param array        $form
	 * @param GF_Field     $field
	 * @param array        $lead
	 * @param array        $current_fields
	 * @param string       $input_id
	 * @param string       $item_index
	 */
	public static function queue_save_input_value( $value, $form, $field, &$lead, $current_fields, $input_id, $item_index = '' ) {

		$input_name = 'input_' . str_replace( '.', '_', $input_id );
		if ( is_array( $value ) && ! ( $field->is_value_submission_array() && ! is_array( rgar( $value, 0 ) ) ) ) {
			foreach ( $value as $i => $v ) {
				$new_item_index = $item_index . '_' . $i;
				if ( is_array( $v ) && ! ( $field->is_value_submission_array() && ! is_array( $v[0] ) ) ) {
					self::queue_save_input_value( $v, $form, $field, $lead, $current_fields, $input_id, $new_item_index );
					continue;
				}
				//processing values so that they are in the correct format for each input type
				$v = self::prepare_value( $form, $field, $v, $input_name, rgar( $lead, 'id' ) );

				$lead_detail_id               = self::get_lead_detail_id( $current_fields, $input_id, $new_item_index );
				$result                       = self::queue_batch_field_operation( $form, $lead, $field, $lead_detail_id, $input_id, $v, $new_item_index );
				GFCommon::log_debug( __METHOD__ . "(): Saving: {$field->label}(#{$input_id}{$item_index} - {$field->type}). Result: " . var_export( $result, 1 ) ); // phpcs:ignore QITStandard.PHP.DebugCode.DebugFunctionFound
				foreach ( $current_fields as $current_field ) {
					if ( (string) $current_field->meta_key === (string) $input_id && $current_field->item_index == $new_item_index ) {
						$current_field->update = true;
					}
				}
			}

		} else {
			//processing values so that they are in the correct format for each input type
			$value = self::prepare_value( $form, $field, $value, $input_name, rgar( $lead, 'id' ), $lead );

			// Fix for implicit conversion from float to int in depreciation notice in PHP 8.1+
			$input_id = (string) $input_id;

			//ignore fields that have not changed
			if ( $lead != null && isset( $lead[ $input_id ] ) && $value === rgget( (string) $input_id, $lead ) ) {
				return;
			}

			$lead_detail_id = self::get_lead_detail_id( $current_fields, $input_id );
			$result         = self::queue_batch_field_operation( $form, $lead, $field, $lead_detail_id, $input_id, $value );
			GFCommon::log_debug( __METHOD__ . "(): Queued field operation: {$field->label}(#{$input_id} - {$field->type})." );

		}
	}

	/**
	 * Updates an existing field value in the database.
	 *
	 * @param array $form
	 * @param array $lead
	 * @param GF_Field $field
	 * @param int $lead_detail_id
	 * @param string $input_id
	 * @param string $value
	 *
	 * @return bool
	 */
	public static function update_lead_field_value( $form, $lead, $field, $lead_detail_id, $input_id, $value ) {
		return self::update_entry_field_value( $form, $lead, $field, $lead_detail_id, $input_id, $value );
	}

	/**
	 * Updates an existing field value in the database.
	 *
	 * @since 2.3
	 *
	 * @param array $form
	 * @param array $entry
	 * @param GF_Field $field
	 * @param int $entry_meta_id
	 * @param string $input_id
	 * @param string $value
	 *
	 * @return bool
	 */
	public static function update_entry_field_value( $form, $entry, $field, $entry_meta_id, $input_id, $value, $item_index = '' ) {
		global $wpdb;

		if ( version_compare( self::get_database_version(), '2.3-dev-1', '<' ) ) {
			return GF_Forms_Model_Legacy::update_lead_field_value( $form, $entry, $field, $entry_meta_id, $input_id, $value );
		}

		/**
		 * Filter the value before it's saved to the database.
		 *
		 * @since 1.5.0
		 * @since 1.8.6 Added the $input_id parameter.
		 * @since 1.9.14 Added form and field specific versions.
		 *
		 * @param string|array $value The fields input value.
		 * @param array $entry The current entry object.
		 * @param GF_Field $field The current field object.
		 * @param array $form The current form object.
		 * @param string $input_id The ID of the input being saved or the field ID for single input field types.
		 */
		$value = apply_filters( 'gform_save_field_value', $value, $entry, $field, $form, $input_id );
		$value = apply_filters( "gform_save_field_value_{$form['id']}", $value, $entry, $field, $form, $input_id );

		if ( is_object( $field ) ) {
			$value = apply_filters( "gform_save_field_value_{$form['id']}_{$field->id}", $value, $entry, $field, $form, $input_id );
		}

		if ( is_array( $value ) ) {
			GFCommon::log_debug( __METHOD__ . '(): bailing. value is an array.' );
			return false;
		}

		$entry_id                = $entry['id'];
		$form_id                = $form['id'];
		$entry_meta_table_name      = self::get_entry_meta_table_name();

		// Add emoji support.
		if ( version_compare( get_bloginfo( 'version' ), '4.2', '>=' ) ) {

			// Get charset for lead detail value column .
			$charset = $wpdb->get_col_charset( $entry_meta_table_name, 'meta_value' );

			// If entry detail value column is UTF-8, encode emoji.
			if ( 'utf8' === $charset ) {
				$value = wp_encode_emoji( $value );
			}
		}

		if ( ! rgblank( $value ) ) {

			if ( $entry_meta_id > 0 ) {

				$result = $wpdb->update( $entry_meta_table_name, array( 'meta_value' => $value ), array( 'id' => $entry_meta_id ), array( '%s' ), array( '%d' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.SlowDBQuery.slow_db_query_meta_value
				if ( false === $result ) {
					return false;
				}

			} else {
				$result = $wpdb->insert( $entry_meta_table_name, array( 'entry_id' => $entry_id, 'form_id' => $form_id, 'meta_key' => $input_id, 'meta_value' => $value, 'item_index' => $item_index ), array( '%d', '%d', '%s', '%s', '%s' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.SlowDBQuery.slow_db_query_meta_value, WordPress.DB.SlowDBQuery.slow_db_query_meta_key
				if ( false === $result ) {
					return false;
				}

			}

		} else {
			// when the value is empty and no $entry_meta_id was set, check if it's a repeater field.
			if ( empty( $entry_meta_id ) && $field instanceof GF_Field_Repeater && isset( $field->fields ) && is_array( $field->fields ) ) {
				foreach ( $field->fields as $subfield ) {
					self::update_entry_field_value( $form, $entry, $subfield, 0, $subfield->id, '' );
				}
			} else {
				// Deleting details for this field
				if ( is_a( $field, 'GF_Field' ) && is_array( $field->get_entry_inputs() ) ) {
					$_input_id = ( false === strpos( $input_id, '.' ) ) ? sprintf( '%d.%%', $input_id ) : $input_id;
					$sql = $wpdb->prepare( "DELETE FROM %i WHERE entry_id=%d AND meta_key LIKE %s ", $entry_meta_table_name, $entry_id, $_input_id );
				} else {
					$sql = $wpdb->prepare( "DELETE FROM %i WHERE entry_id=%d AND meta_key = %s ", $entry_meta_table_name, $entry_id, $input_id );
				}
				if ( $item_index ) {
					$sql .= $wpdb->prepare( ' AND item_index=%s', $item_index );
				}
				$result = $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
				if ( false === $result ) {
					return false;
				}
			}
		}

		return true;
	}

	/**
	 * Returns the SQL to update or insert field values..
	 *
	 * @param array    $form
	 * @param array    $entry
	 * @param GF_Field $field
	 * @param int      $entry_meta_id
	 * @param string   $input_id
	 * @param string   $value
	 * @param string   $item_index
	 *
	 * @return bool
	 */
	public static function queue_batch_field_operation( $form, &$entry, $field, $entry_meta_id, $input_id, $value, $item_index = '' ) {
		/**
		 * Filter the value before it's saved to the database.
		 *
		 * @since 1.5.0
		 * @since 1.8.6 Added the $input_id parameter.
		 * @since 1.9.14 Added form and field specific versions.
		 *
		 * @param string|array $value The fields input value.
		 * @param array $entry The current entry object.
		 * @param GF_Field $field The current field object.
		 * @param array $form The current form object.
		 * @param string $input_id The ID of the input being saved or the field ID for single input field types.
		 */
		$value = apply_filters( 'gform_save_field_value', $value, $entry, $field, $form, $input_id );
		$value = apply_filters( "gform_save_field_value_{$form['id']}", $value, $entry, $field, $form, $input_id );

		if ( is_object( $field ) ) {
			$value = apply_filters( "gform_save_field_value_{$form['id']}_{$field->id}", $value, $entry, $field, $form, $input_id );
		}

		if ( is_array( $value ) ) {
			GFCommon::log_debug( __METHOD__ . '(): bailing. value is an array.' );
			return false;
		}

		$entry[ (string) $input_id . $item_index ] = $value;

		$entry_id = $entry['id'];
		$form_id  = $form['id'];

		if ( ! rgblank( $value ) ) {
			if ( $entry_meta_id > 0 ) {
				self::$_batch_field_updates[] = array( 'meta_value' => $value, 'id' => $entry_meta_id ); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
			} else {
				self::$_batch_field_inserts[] = array( 'entry_id' => $entry_id, 'form_id' => $form_id, 'meta_key' => $input_id, 'meta_value' => $value, 'item_index' => $item_index ); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value, WordPress.DB.SlowDBQuery.slow_db_query_meta_key
			}
		} elseif ( $entry_meta_id > 0 && ! in_array( $input_id, GFFormsModel::get_lead_db_columns() ) ) {
			self::$_batch_field_deletes[] = $entry_meta_id;
		}

		return true;
	}

	/**
	 * Checks if any field updates, inserts, or deletions have been registered for batch processing.
	 *
	 * @since 2.4.17
	 *
	 * @return bool
	 */
	public static function has_batch_field_operations() {
		return ! empty( self::$_batch_field_updates ) || ! empty( self::$_batch_field_inserts ) || ! empty( self::$_batch_field_deletes );
	}

	public static function flush_batch_field_operations() {
		self::$_batch_field_updates = array();
		self::$_batch_field_inserts = array();
		self::$_batch_field_deletes = array();
	}

	public static function begin_batch_field_operations() {
		self::flush_batch_field_operations();
	}


	/**
	 * Performs the update, inserts and deletes registered by queue_batch_field_operation()
	 *
	 * @return array An array of results.
	 */
	public static function commit_batch_field_operations() {
		global $wpdb;

		$meta_table = self::get_entry_meta_table_name();

		$results = array(
			'updates' => null,
			'inserts' => null,
			'deletes' => null,
		);

		// Updates
		if ( ! empty( self::$_batch_field_updates ) ) {
			$values = array();
			foreach ( self::$_batch_field_updates as $update ) {
				$values[] = $wpdb->prepare( '(%s,%s)', $update['id'], $update['meta_value'] );
			}
			$values_str = join( ',', $values );
			$update_sql =  "INSERT INTO {$meta_table} (id,meta_value)
						VALUES {$values_str}
						ON DUPLICATE KEY UPDATE meta_value=VALUES(meta_value);";
			$result = $wpdb->query( $update_sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			if ( $result === false ) {
				$result = new WP_Error( 'update_error', $wpdb->last_error );
			}
			$results['updates'] = $result;
		}

		// Inserts
		if ( ! empty( self::$_batch_field_inserts ) ) {
			$values = array();
			foreach ( self::$_batch_field_inserts as $insert ) {
				$values[] = $wpdb->prepare( '(%d,%d,%s,%s,%s)', $insert['entry_id'], $insert['form_id'], $insert['meta_key'], $insert['meta_value'], $insert['item_index'] );
			}
			$values_str = join( ',', $values );
			$insert_sql = "INSERT INTO {$meta_table} (entry_id, form_id, meta_key, meta_value, item_index)  VALUES {$values_str};";
			$result = $wpdb->query( $insert_sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			if ( $result === false ) {
				$result = new WP_Error( 'insert_error', $wpdb->last_error );
			}
			$results['inserts'] = $result;
		}

		// Deletes
		if ( ! empty( self::$_batch_field_deletes ) ) {
			$in_str_arr    = array_fill( 0, count( self::$_batch_field_deletes ), '%d' );
			$in_str        = join( ',', $in_str_arr );
			$ids = array_map( 'absint', self::$_batch_field_deletes );
			$delete_sql = $wpdb->prepare( "DELETE FROM {$meta_table} WHERE id IN ( {$in_str} )", $ids); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
			$result = $wpdb->query( $delete_sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			if ( $result === false ) {
				$result = new WP_Error( 'delete_error', $wpdb->last_error );
			}
			$results['deletes'] = $result;
		}

		self::flush_batch_field_operations();

		return $results;
	}

	/**
	 * @depecated 1.9
	 * @remove-in 3.0
	 */
	private static function move_temp_file( $form_id, $tempfile_info ) {
		_deprecated_function( 'move_temp_file', '1.9', 'GF_Field_Fileupload::move_temp_file' );

		$target       = self::get_file_upload_path( $form_id, $tempfile_info['uploaded_filename'] );
		$tmp_location = GFFormsModel::get_tmp_upload_location( $form_id );
		$source       = $tmp_location['path'] . $tempfile_info['temp_filename'];

		if ( rename( $source, $target['path'] ) ) {
			self::set_permissions( $target['path'] );

			return $target['url'];
		} else {
			return 'FAILED (Temporary file could not be moved.)';
		}
	}

	public static function set_permissions( $path ) {
		$permission = apply_filters( 'gform_file_permission', 0644, $path );
		if ( $permission ) {
			@chmod( $path, $permission );
		}
	}

	/**
	 * @depecated 1.9
	 * @remove-in 3.0
	 */
	public static function upload_file( $form_id, $file ) {
		_deprecated_function( 'upload_file', '1.9', 'GF_Field_Fileupload::upload_file' );
		$target = self::get_file_upload_path( $form_id, $file['name'] );
		if ( ! $target ) {
			GFCommon::log_debug( 'GFFormsModel::upload_file(): FAILED (Upload folder could not be created.)' );

			return 'FAILED (Upload folder could not be created.)';
		}


		if ( move_uploaded_file( $file['tmp_name'], $target['path'] ) ) {
			GFCommon::log_debug( 'GFFormsModel::upload_file(): Setting permissions on ' . $target['path'] );
			self::set_permissions( $target['path'] );

			return $target['url'];
		} else {
			GFCommon::log_debug( 'GFFormsModel::upload_file(): FAILED (Temporary file could not be copied.)' );

			return 'FAILED (Temporary file could not be copied.)';
		}
	}


	public static function get_upload_root() {
		$dir = wp_upload_dir();

		if ( $dir['error'] ) {
			return null;
		}

		return $dir['basedir'] . '/gravity_forms/';
	}

	public static function get_upload_url_root() {
		$dir = wp_upload_dir();

		if ( $dir['error'] ) {
			return null;
		}

		return $dir['baseurl'] . '/gravity_forms/';
	}

	public static function get_upload_path( $form_id ) {
		$form_id = absint( $form_id );
		return self::get_upload_root() . $form_id . '-' . wp_hash( $form_id );
	}

	/*
	 * Get the path to the temporary upload directory for a form.
	 *
	 * @since 2.9.3
	 * @since 2.9.18 Added static caching.
	 *
	 * @param int $form_id The ID of the form.
	 *
	 * @return array The path and url to the temporary upload directory for a form.
	 */
	public static function get_tmp_upload_location( $form_id ) {
		static $locations = array();

		if ( ! isset( $locations[ $form_id ] ) ) {
			$location = array(
				'path' => self::get_upload_path( $form_id ) . DIRECTORY_SEPARATOR . 'tmp' . DIRECTORY_SEPARATOR,
				'url'  => self::get_upload_url( $form_id ) . '/tmp/',
			);

			/*
			 * Filter the temporary upload directory path for a form
			 *
			 * @since 2.9.3
			 *
			 * @param array $location An array containing the path and url of the temporary upload directory.
			 * @param int   $form_id  The ID of the form.
			 */
			$locations[ $form_id ] = (array) gf_apply_filters( array( 'gform_file_upload_tmp_dir', $form_id ), $location, $form_id );
		}

		return $locations[ $form_id ];
	}

	public static function get_upload_url( $form_id ) {
		$form_id = absint( $form_id );
		$dir = wp_upload_dir();

		return $dir['baseurl'] . "/gravity_forms/$form_id" . '-' . wp_hash( $form_id );
	}

	public static function get_file_upload_path( $form_id, $file_name, $increment_found = true ) {

		if ( version_compare( phpversion(), '7.4', '<' ) && get_magic_quotes_gpc() ) {
			$file_name = stripslashes( $file_name );
		}
		$form_id = absint( $form_id );
		//adding filter to upload root path and url
		$upload_root_info    = GF_Field_FileUpload::get_upload_root_info( $form_id );
		$target_root         = $upload_root_info['path'];
		$target_root_url     = $upload_root_info['url'];

		$default_upload_root_info =  GF_Field_FileUpload::get_default_upload_roots( $form_id );
		$default_target_root      = rgar( $default_upload_root_info, 'path' );
		$y                        = rgar( $default_upload_root_info, 'y' );
		$m                        = rgar( $default_upload_root_info, 'm' );


		$target_root = trailingslashit( $target_root );

		if ( ! is_dir( $target_root ) ) {
			if ( ! wp_mkdir_p( $target_root ) ) {
				return false;
			}

			// Adding index.html files to all subfolders.
			if ( $default_target_root != $target_root && ! file_exists( $target_root . 'index.html' ) ) {
				GFCommon::recursive_add_index_file( $target_root );
			} elseif ( ! file_exists( self::get_upload_root() . '/index.html' ) ) {
				GFCommon::recursive_add_index_file( self::get_upload_root() );
			} elseif ( ! file_exists( self::get_upload_path( $form_id ) . '/index.html' ) ) {
				GFCommon::recursive_add_index_file( self::get_upload_path( $form_id ) );
			} elseif ( ! file_exists( self::get_upload_path( $form_id ) . "/$y/index.html" ) ) {
				GFCommon::recursive_add_index_file( self::get_upload_path( $form_id ) . "/$y" );
			} else {
				GFCommon::recursive_add_index_file( self::get_upload_path( $form_id ) . "/$y/$m" );
			}
		}

		$file_name = sanitize_file_name( $file_name );

		//Add the original filename to our target path.
		//Result is "uploads/filename.extension"
		$extension = pathinfo( $file_name, PATHINFO_EXTENSION );
		if ( ! empty( $extension ) ) {
			$extension = '.' . $extension;
		}

		$file_name = wp_basename( $file_name, $extension );

		$counter     = 1;
		$target_path = $target_root . $file_name . $extension;
		while ( $increment_found && file_exists( $target_path ) ) {
			$target_path = $target_root . $file_name . "$counter" . $extension;
			$counter ++;
		}

		//Remove '.' from the end if file does not have a file extension
		$target_path = trim( $target_path, '.' );

		//creating url
		$target_url = str_replace( $target_root, $target_root_url, $target_path );

		return array( 'path' => $target_path, 'url' => $target_url );
	}

	public static function get_tables() {
		return array(
			self::get_form_view_table_name(),
			self::get_meta_table_name(),
			self::get_form_table_name(),
			self::get_form_revisions_table_name(),
			self::get_entry_table_name(),
			self::get_entry_meta_table_name(),
			self::get_entry_notes_table_name(),
			self::get_draft_submissions_table_name(),
			self::get_rest_api_keys_table_name(),
		);
	}

	public static function drop_tables() {
		global $wpdb;
		foreach ( GF_Forms_Model_Legacy::get_legacy_tables() as $table ) {
			$wpdb->query( $wpdb->prepare( "DROP TABLE IF EXISTS %i", $table ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
		}
		foreach ( self::get_tables() as $table ) {
			$wpdb->query( $wpdb->prepare( "DROP TABLE IF EXISTS %i", $table ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
		}
	}

	/**
	 * Target for the wpmu_drop_tables filter. Adds all tables for Gravity Forms and the Add-On Framework to list
	 * of tables to drop when a site is deleted.
	 *
	 * @param $drop_tables
	 *
	 * @return array
	 */
	public static function mu_drop_tables( $drop_tables ) {
		global $wpdb;

		$addon_tables = array(
			self::get_addon_feed_table_name(),
			$wpdb->prefix . 'gf_addon_payment_callback',
			$wpdb->prefix . 'gf_addon_payment_transaction',
		);

		$drop_tables = array_merge( $drop_tables, $addon_tables );

		$core_tables = self::get_tables();

		$drop_tables = array_merge( $drop_tables, $core_tables );

		$legacy_tables = GF_Forms_Model_Legacy::get_legacy_tables();

		$drop_tables = array_merge( $drop_tables, $legacy_tables );

		return $drop_tables;
	}

	public static function insert_form_view( $form_id, $deprecated = null ) {
		global $wpdb;
		$table_name = self::get_form_view_table_name();

		$sql = $wpdb->prepare( "SELECT id FROM %i WHERE form_id=%d AND date_created BETWEEN DATE_SUB(utc_timestamp(), INTERVAL 1 DAY) AND utc_timestamp()", $table_name, $form_id );

		$id = $wpdb->get_var( $sql, 0, 0 ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

		if ( empty( $id ) ) {
			$wpdb->query( $wpdb->prepare( "INSERT INTO %i(form_id, date_created, ip) values(%d, utc_timestamp(), %s)", $table_name, $form_id, '' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		} else {
			$wpdb->query( $wpdb->prepare( "UPDATE %i SET count = count+1 WHERE id=%d", $table_name, $id ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		}
	}

	public static function is_duplicate( $form_id, $field, $value ) {
		global $wpdb;

		if ( version_compare( self::get_database_version(), '2.3-dev-1', '<' ) ) {
			return GF_Forms_Model_Legacy::is_duplicate( $form_id, $field, $value );
		}

		$entry_meta_table_name   = self::get_entry_meta_table_name();
		$entry_table_name        = self::get_entry_table_name();
		$sql_comparison          = 'ld.meta_value = %s';

		switch ( GFFormsModel::get_input_type( $field ) ) {
			case 'time':
				$value = sprintf( "%02d:%02d %s", $value[0], $value[1], $value[2] );
				break;
			case 'date':
				$value = self::prepare_date( $field->dateFormat, $value );
				break;
			case 'number':
				$value = GFCommon::clean_number( $value, $field->numberFormat );
				break;
			case 'phone':
				$value          = str_replace( array( ')', '(', '-', ' ' ), '', $value );
				$sql_comparison = 'replace( replace( replace( replace( ld.meta_value, ")", "" ), "(", "" ), "-", "" ), " ", "" ) = %s';
				break;
			case 'email':
				$value = is_array( $value ) ? rgar( $value, 0 ) : $value;
				break;
		}

		$inner_sql_template = "SELECT %s as input, ld.entry_id
                                FROM {$entry_meta_table_name} ld
                                INNER JOIN {$entry_table_name} l ON l.id = ld.entry_id\n";


		$inner_sql_template .= "WHERE l.form_id=%d AND ld.form_id=%d
                                AND ld.meta_key = %s
                                AND status='active' AND {$sql_comparison}";

		$sql = "SELECT count(distinct input) as match_count FROM ( ";

		$input_count = 1;
		if ( is_array( $field->get_entry_inputs() ) ) {
			$input_count = sizeof( $field->inputs );
			$inner_sql = '';
			foreach ( $field->inputs as $input ) {
				$union = empty( $inner_sql ) ? '' : ' UNION ALL ';
				$inner_sql .= $union . $wpdb->prepare( $inner_sql_template, $input['id'], $form_id, $form_id, $input['id'], $value[ $input['id'] ] ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
			}
		} else {
			$inner_sql = $wpdb->prepare( $inner_sql_template, $field->id, $form_id, $form_id, $field->id, $value ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		}

		$sql .= $inner_sql . "
                ) as count
                GROUP BY entry_id
                ORDER BY match_count DESC";

		$count = gf_apply_filters( array( 'gform_is_duplicate', $form_id ), $wpdb->get_var( $sql ), $form_id, $field, $value ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

		return $count != null && $count >= $input_count;
	}

	public static function get_lead( $lead_id ) {
		$entry = GFAPI::get_entry( $lead_id );
		if ( is_wp_error( $entry ) ) {
			$entry = false;
		}
		return $entry;
	}

	public static function get_entry( $entry_id ) {
		return GFAPI::get_entry( $entry_id );
	}

	public static function get_lead_notes( $lead_id ) {
		global $wpdb;

		if ( version_compare( self::get_database_version(), '2.3-dev-1', '<' ) ) {
			return GF_Forms_Model_Legacy::get_lead_notes( $lead_id );
		}

		$notes_table = self::get_entry_notes_table_name();

		return $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			$wpdb->prepare(
				"SELECT n.id, n.user_id, n.date_created, n.value, n.note_type, n.sub_type, ifnull(u.display_name,n.user_name) as user_name, u.user_email
					FROM %i n
					LEFT OUTER JOIN $wpdb->users u ON n.user_id = u.id
					WHERE entry_id=%d ORDER BY id", $notes_table, $lead_id
			)
		);
	}

	/**
	 * Get a collection of notes.
	 *
	 * @since 2.4.18
	 *
	 * @param array      $search_criteria {
	 * 		Array of search criteria.
	 *
	 * 		@type int       $id         Get the note with this ID.
	 * 		@type int|array $entry_id   Get notes associated with this entry ID or array of IDs.
	 * 		@type int       $user_id    Get notes with this user ID.
	 * 		@type string    $user_name  Get notes with this user name.
	 * 		@type string    $note_type  Get notes with this note type.
	 * 		@type string    $sub_type   Get notes with this sub type.
	 * 		@type string    $start_date Get notes on or after this date.  Expects SQL datetime format.
	 * 		@type string    $end_date   Get notes on or before this date.  Expects SQL datetime format.
	 * }
	 * @param null|array $sorting {
	 * 		Array of sort key and direction.
	 *
	 * 		@type string $key       Key to sort on.  Options: id, entry_id, user_id, user_name, note_type, sub_type, date.
	 * 		@type string $direction Sort direction.  Options: ASC, DESC.
	 * }
	 * @return array|null|object
	 */
	public static function get_notes( $search_criteria = array(), $sorting = null ) {
		global $wpdb;

		$where = array();

		if ( rgar( $search_criteria, 'id' ) ) {
			$where[] = $wpdb->prepare( 'n.id = %d', $search_criteria['id'] );
		}

		$entry_criteria = rgar( $search_criteria, 'entry_id' );
		if ( $entry_criteria && is_array( $search_criteria['entry_id'] ) ) {
			$entry_ids    = array_map( 'intval', $search_criteria['entry_id'] );
			$placeholders = implode( ', ', array_fill( 0, count( $entry_ids ), '%d' ) );
			$where[]      = $wpdb->prepare( "entry_id IN ($placeholders)", $entry_ids ); // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		} elseif( $entry_criteria ) {
			$where[] = $wpdb->prepare( 'entry_id = %d', $search_criteria['entry_id'] );
		}

		if ( rgars( $search_criteria, 'user_id' ) ) {
			$where[] = $wpdb->prepare( 'user_id = %d', $search_criteria['user_id'] );
		}

		if ( isset( $search_criteria['user_name'] ) ) {
			if ( '' !== $search_criteria['user_name'] ) {
				$where[] = $wpdb->prepare('user_name = %s', $search_criteria['user_name']);
			} else {
				$where[] = "( user_name = '' OR user_name IS NULL )";
			}
		}

		if ( rgar( $search_criteria, 'note_type' ) ) {
			$where[] = $wpdb->prepare( 'note_type = %s', $search_criteria['note_type'] );
		}

		if ( isset( $search_criteria['sub_type'] ) ) {
			if ( '' !== $search_criteria['sub_type'] ) {
				$where[] = $wpdb->prepare('sub_type = %s', $search_criteria['sub_type']);
			} else {
				$where[] = "( sub_type = '' OR sub_type IS NULL )";
			}
		}

		if ( rgar( $search_criteria, 'start_date' ) ) {
			if ( ! is_numeric( $search_criteria['start_date'] ) || (int) $search_criteria['start_date'] != $search_criteria['start_date'] ) {
				$search_criteria['start_date'] = strtotime( $search_criteria['start_date'] );
			}
			$valid_timestamp = gmdate( 'Y-m-d H:i:s', $search_criteria['start_date'] );
			$where[]         = $wpdb->prepare( 'timestampdiff(SECOND, %s, date_created) >= 0', $valid_timestamp );
		}

		if ( rgar( $search_criteria, 'end_date' ) ) {
			if ( ! is_numeric( $search_criteria['end_date'] ) || (int) $search_criteria['end_date'] != $search_criteria['end_date'] ) {
				$search_criteria['end_date'] = strtotime( $search_criteria['end_date'] );
			}
			$valid_timestamp = gmdate( 'Y-m-d H:i:s', $search_criteria['end_date'] );
			// The user didn't specify and end time, so search until the end of the day.
			if ( '00:00:00' == substr( $valid_timestamp, -8 ) ) {
				$valid_timestamp = gmdate( 'Y-m-d', $search_criteria['end_date'] ) . ' 23:59:59';
			}
			$where[] = $wpdb->prepare( 'timestampdiff(SECOND, %s, date_created) <= 0', $valid_timestamp );
		}

		$where = 'WHERE ' . implode( ' AND ', $where );

		if ( empty( $search_criteria ) ) {
			$where = '';
		}

		if ( is_array( $sorting ) && ! empty( $sorting ) ) {
			$sorting_options = array( 'entry_id', 'id', 'user_id', 'date_created', 'value', 'note_type', 'sub_type', 'user_name', 'user_email' );
			if ( ! isset( $sorting['key'] ) || ! in_array( $sorting['key'], $sorting_options ) ) {
				$sorting['key'] = 'n.id';
			}

			if ( 'id' == $sorting['key'] ) {
				$sorting['key'] = 'n.id';
			}
			$direction_options = array( 'ASC', 'DESC' );
			if ( ! isset( $sorting['direction'] ) || ! in_array( $sorting['direction'], $direction_options )  ) {
				$sorting['direction'] = 'ASC';
			}
			$orderby = 'ORDER BY ' . $sorting['key'] . ' ' . $sorting['direction'];
		} else {
			$orderby = 'ORDER BY n.id ASC';
		}

		$notes_table = self::get_entry_notes_table_name();

		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		return $wpdb->get_results(
			"SELECT n.entry_id, n.id, n.user_id, n.date_created, n.value, n.note_type, n.sub_type, ifnull(u.display_name,n.user_name) as user_name, u.user_email
				FROM $notes_table n
				LEFT OUTER JOIN $wpdb->users u ON n.user_id = u.id
				$where
				$orderby"
		);
		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
	}

	public static function refresh_lead_field_value( $lead_id, $field_id ) {
		if ( version_compare( GFForms::$version, '2.3-dev', '>=' ) ) {
			_deprecated_function( 'GFFormsModel::refresh_lead_field_value', '2.3' );
		}
		$cache_key = 'GFFormsModel::get_lead_field_value_' . $lead_id . '_' . $field_id;
		GFCache::delete( $cache_key );
	}

	/**
	 * @param $lead
	 * @param $field GF_Field
	 *
	 * @return array|bool|mixed|string|null
	 */
	public static function get_lead_field_value( $lead, $field ) {

		if ( empty( $lead ) || ! is_array( $lead ) ) {
			return null;
		}

		$field_id = $field instanceof GF_Field ? $field->id : rgar( $field, 'id' );

		$value = array();

		$inputs = $field instanceof GF_Field ? $field->get_entry_inputs() : rgar( $field, 'inputs' );

		if ( is_array( $inputs ) ) {
			// making sure values submitted are sent in the value even if
			// there isn't an input associated with it
			$lead_field_keys = array_keys( $lead );
			// We don't reorder the keys for Choice or Image Choice fields as they need to be rendered in the same order as they are saved in DB.
			if ( ! $field->has_persistent_choices() ) {
				natsort( $lead_field_keys );
			}
			foreach ( $lead_field_keys as $input_id ) {
				if ( is_numeric( $input_id ) && absint( $input_id ) == absint( $field_id ) ) {
					$val = $lead[ $input_id ];
					$value[ $input_id ] = $val;
				}
			}
		} else {
			$value = rgget( $field_id, $lead );
		}

		// filtering lead value
		$value = apply_filters( 'gform_get_field_value', $value, $lead, $field );

		return $value;
	}

	/**
	 *
	 * @deprecated 2.0
	 * @remove-in 3.0
	 *
	 * @param      $lead
	 * @param      $field_number
	 * @param      $form
	 * @param bool $apply_filter
	 *
	 * @return mixed|null|string
	 */
	public static function get_field_value_long( $lead, $field_number, $form, $apply_filter = true ) {
		_deprecated_function( 'get_field_value_long', '2.0', 'get_lead_field_value' );

		global $wpdb;
		$detail_table_name = self::get_lead_details_table_name();
		$long_table_name   = self::get_lead_details_long_table_name();

		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$sql = $wpdb->prepare(
			" SELECT l.value FROM $detail_table_name d
                                INNER JOIN $long_table_name l ON l.lead_detail_id = d.id
                                WHERE lead_id=%d AND field_number BETWEEN %s AND %s", $lead['id'], doubleval( $field_number ) - 0.0001, doubleval( $field_number ) + 0.0001
		);
		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$val = $wpdb->get_var( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

		//running aform_get_input_value when needed
		if ( $apply_filter ) {
			$field    = RGFormsModel::get_field( $form, $field_number );
			$input_id = (string) $field_number == (string) $field->id ? '' : $field_number;
			$val      = gf_apply_filters( array( 'gform_get_input_value', $field->formId, $field->id, $input_id ), $val, $lead, $field, $input_id );
		}

		return $val;
	}

	/**
	 * @param $meta_key
	 * @param $meta_value
	 *
	 * @return array
	 */
	public static function get_leads_by_meta( $meta_key, $meta_value ) {
		return self::get_entries_by_meta( $meta_key, $meta_value );
	}

	/**
	 * Searches entries by entry meta
	 *
	 * @since 2.3
	 *
	 * @param $meta_key
	 * @param $meta_value
	 *
	 * @return array
	 */
	public static function get_entries_by_meta( $meta_key, $meta_value ) {
		if ( version_compare( self::get_database_version(), '2.3-dev-1', '<' ) ) {
			return GF_Forms_Model_Legacy::get_leads_by_meta( $meta_key, $meta_value );
		}

		// phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.SlowDBQuery.slow_db_query_meta_value
		$args = array(
			'meta_key'     => $meta_key,
			'meta_value'   => $meta_value,
			'meta_compare' => '=',
		);
		// phpcs:enable WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.SlowDBQuery.slow_db_query_meta_value

		$query = new GF_Query( $args );
		return $query->entries;
	}

	/**
	 *
	 * @deprecated 2.3
	 * @remove-in 3.0
	 *
	 * @param $form_id
	 * @param int $sort_field_number
	 * @param string $sort_direction
	 * @param string $search
	 * @param int $offset
	 * @param int $page_size
	 * @param null $star
	 * @param null $read
	 * @param bool $is_numeric_sort
	 * @param null $start_date
	 * @param null $end_date
	 * @param string $status
	 * @param bool $payment_status
	 *
	 * @return mixed
	 */
	public static function get_leads( $form_id, $sort_field_number = 0, $sort_direction = 'DESC', $search = '', $offset = 0, $page_size = 30, $star = null, $read = null, $is_numeric_sort = false, $start_date = null, $end_date = null, $status = 'active', $payment_status = false ) {

		_deprecated_function( 'GFFormsModel::get_leads', '2.3', 'GFAPI::get_entries' );

		$search_criteria = array(
			'status' => $status,
		);

		if ( ! empty( $search ) ) {
			$search_criteria['field_filters'][] = array( 'value' => $search );
		}

		if ( ! is_null( $star ) ) {
			$search_criteria['field_filters'][] = array( 'is_starred' => $star );
		}

		if ( ! is_null( $read ) ) {
			$search_criteria['field_filters'][] = array( 'is_read' => $read );
		}

		if ( $payment_status ) {
			$search_criteria['field_filters'][] = array( 'payment_status' => $read );
		}

		$sorting = array(
			'key' => $sort_field_number,
			'direction' => $sort_direction
		);

		if ( $is_numeric_sort ) {
			$sorting['is_numeric'] = true;
		}

		$paging = array(
			'offset' => $offset,
			'page_size' => $page_size,
		);

		if ( ! is_null( $start_date ) ) {
			$search_criteria['start_date'] = $start_date;
		}

		if ( ! is_null( $end_date ) ) {
			$search_criteria['end_date'] = $end_date;
		}

		return GFAPI::get_entries( $form_id, $search_criteria, $sorting, $paging );
	}


	/**
	 *
	 * @deprecated 2.3
	 * @remove-in 3.0
	 *
	 * @param $args
	 *
	 * @return string
	 */
	public static function get_leads_where_sql( $args ) {

		if ( version_compare( self::get_database_version(), '2.3-dev-1', '<' ) ) {
			return GF_Forms_Model_Legacy::get_leads_where_sql( $args ) ;
		}

		return self::get_entries_where_sql( $args );
	}

	/**
	 * @deprecated 2.3
	 * @remove-in 3.0
	 *
	 * @param $results
	 *
	 * @return array
	 */
	public static function build_lead_array( $results ) {
		_deprecated_function(__METHOD__, '2.3');
		return GF_Forms_Model_Legacy::build_lead_array( $results );
	}

	/***
	 * Saves the Gravity Forms license key to the database and registers the site and license key with the Gravity Forms licensing server.
	 *
	 * @since 1.0
	 *
	 * @param string $new_key Gravity Forms license key to be saved.
	 */
	public static function save_key( $new_key ) {

		$new_key      = trim( $new_key );
		$new_key_md5  = md5( $new_key );
		$previous_key = get_option( GFForms::LICENSE_KEY_OPT );

		/**
		 * @var License\GF_License_API_Connector $license_connector
		 */
		$license_connector = GFForms::get_service_container()->get( License\GF_License_Service_Provider::LICENSE_API_CONNECTOR );
		$license_connector->clear_cache_for_key( $new_key_md5 );

		// Delete gform_version_info so GF will ping version.php to send site record update.
		delete_option( 'gform_version_info' );

		if ( empty( $new_key ) ) {
			self::update_license_key( '' );

			// Unlink the site with the license key on Gravity API.
			$license_connector->update_site_registration( '' );

		} elseif ( $previous_key != $new_key ) {
			self::update_license_key( $new_key_md5 );

			// Updating site registration with Gravity Server.
			$result = $license_connector->update_site_registration( $new_key_md5, true );

			// New key is invalid, revert to old key.
			if ( ! $result->can_be_used() ) {
				self::update_license_key( $previous_key );
			}
		} else {
			// Updating site registration with Gravity Server.
			$license_connector->update_site_registration( $new_key_md5, true );
		}

	}

	/**
	 * Use GFAPI::count_entries() instead.
	 *
	 * @deprecated 2.3.0.1
	 * @remove-in 3.0
	 *
	 *
	 * @param $form_id
	 * @param $search
	 * @param null $star
	 * @param null $read
	 * @param null $start_date
	 * @param null $end_date
	 * @param null $status
	 * @param null $payment_status
	 *
	 * @return null|string
	 */
	public static function get_lead_count( $form_id, $search, $star = null, $read = null, $start_date = null, $end_date = null, $status = null, $payment_status = null ) {

		_deprecated_function( 'GFFormsModel::get_lead_count', '2.3.0.1', 'GFAPI::count_entries');

		global $wpdb;

		if ( version_compare( self::get_database_version(), '2.3-dev-1', '<' ) ) {
			return GF_Forms_Model_Legacy::get_lead_count( $form_id, $search, $star, $read, $start_date, $end_date, $status, $payment_status ) ;
		}

		if ( ! is_numeric( $form_id ) ) {
			return '';
		}

		$entry_meta_table_name = self::get_entry_meta_table_name();
		$entry_table_name   = self::get_entry_table_name();

		$where = self::get_entries_where_sql( compact( 'form_id', 'search', 'status', 'star', 'read', 'start_date', 'end_date', 'payment_status', 'is_default' ) );

		$sql = "SELECT count(distinct l.id)
                FROM $entry_table_name l
                INNER JOIN $entry_meta_table_name ld ON l.id = ld.entry_id
                $where";

		return $wpdb->get_var( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
	}

	/**
	 * Returns the WHERE clause for an entry search.
	 *
	 * This function is not used and is only included for backwards compatibility. Use GFAPI::count_entries() instead.
	 *
	 * @deprecated 2.3.0.1
	 * @remove-in 3.0
	 *
	 * @since 2.3.0.1
	 *
	 * @param $args
	 *
	 * @return string
	 */
	public static function get_entries_where_sql( $args ) {

		_doing_it_wrong( 'GFFormsModel::get_entries_where_sql', 'Use GFAPI::count_entries instead', '2.3.0.1');

		global $wpdb;

		extract(
			wp_parse_args(
				$args, array(
					'form_id'        => false,
					'search'         => '',
					'status'         => 'active',
					'star'           => null,
					'read'           => null,
					'start_date'     => null,
					'end_date'       => null,
					'payment_status' => null,
					'is_default'     => true,
				)
			)
		);

		$where = array();

		if ( $is_default ) {
			$where[] = "l.form_id = $form_id";
		}

		if ( $search && $is_default ) {
			$where[] = $wpdb->prepare( 'meta_value LIKE %s', "%$search%" );
		} else if ( $search ) {
			$where[] = $wpdb->prepare( 'd.meta_value LIKE %s', "%$search%" );
		}

		if ( $star !== null && $status == 'active' ) {
			$where[] = $wpdb->prepare( "is_starred = %d AND status = 'active'", $star );
		}

		if ( $read !== null && $status == 'active' ) {
			$where[] = $wpdb->prepare( "is_read = %d AND status = 'active'", $read );
		}

		if ( $payment_status ) {
			$where[] = $wpdb->prepare( "payment_status = '%s'", $payment_status ); // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.QuotedSimplePlaceholder
		}

		if ( $status !== null ) {
			$where[] = $wpdb->prepare( 'status = %s', $status );
		}

		if ( ! empty( $start_date ) ) {
			$where[] = "timestampdiff(SECOND, '$start_date', date_created) >= 0";
		}

		if ( ! empty( $end_date ) ) {
			$where[] = "timestampdiff(SECOND, '$end_date', date_created) <= 0";
		}

		return 'WHERE ' . implode( ' AND ', $where );
	}

	/**
	 *
	 *
	 * @param $form_id
	 * @param $search
	 * @param null $star
	 * @param null $read
	 * @param null $start_date
	 * @param null $end_date
	 * @param null $status
	 * @param null $payment_status
	 *
	 * @return array|string
	 */
	public static function get_lead_ids( $form_id, $search, $star = null, $read = null, $start_date = null, $end_date = null, $status = null, $payment_status = null ) {
		global $wpdb;

		if ( version_compare( self::get_database_version(), '2.3-dev-1', '<' ) ) {
			return GF_Forms_Model_Legacy::get_lead_ids( $form_id, $search, $star, $read, $start_date, $end_date, $status, $payment_status ) ;
		}

		if ( ! is_numeric( $form_id ) ) {
			return '';
		}

		$entry_meta_table_name = self::get_entry_meta_table_name();
		$entry_table_name   = self::get_entry_table_name();

		$where = self::get_entries_where_sql( compact( 'form_id', 'search', 'status', 'star', 'read', 'start_date', 'end_date', 'payment_status', 'is_default' ) );

		$sql = "SELECT distinct l.id
                FROM $entry_table_name l
                INNER JOIN $entry_meta_table_name ld ON l.id = ld.entry_id
                $where";

		$rows = $wpdb->get_results( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

		if ( empty( $rows ) ) {
			return array();
		}

		$entry_ids = array();

		foreach ( $rows as $row ) {
			$entry_ids[] = $row->id;
		}

		return $entry_ids;

	}

	public static function get_grid_columns( $form_id, $input_label_only = false ) {
		if ( empty( $form_id ) ) {
			return array();
		}

		$form = self::get_form_meta( $form_id );
		if ( empty( $form ) ) {
			return array();
		}

		$field_ids = self::get_grid_column_meta( $form_id );

		if ( ! is_array( $field_ids ) ) {
			$field_ids = array();

			foreach ( $form['fields'] as $field ) {
				/* @var GF_Field $field */

				//loading post category fields with choices and inputs
				if ( $field->type == 'post_category' ) {
					$field = GFCommon::add_categories_as_choices( $field, '' );
				}

				if ( $field->displayOnly || $field->get_input_type() == 'list' || $field->get_input_type() == 'repeater' ) {
					continue;
				}

				$inputs = $field->get_entry_inputs();
				if ( is_array( $inputs ) ) {
					if ( $field->type == 'name' ) {
						$field_ids[] = $field->id . '.3'; //adding first name
						$field_ids[] = $field->id . '.6'; //adding last name
					} else {
						foreach ( $inputs as $input ) {
							if ( rgar( $input, 'isHidden' ) ) {
								continue;
							}

							$field_ids[] = $input['id']; //getting first input
							break;
						}
					}
				} else {
					$field_ids[] = $field->id;
				}

				if ( count( $field_ids ) >= 5 ) {
					break;
				}
			}
			//adding default entry meta columns
			$entry_metas = GFFormsModel::get_entry_meta( $form_id );
			foreach ( $entry_metas as $key => $entry_meta ) {
				if ( rgar( $entry_meta, 'is_default_column' ) ) {
					$field_ids[] = $key;
				}
			}
		}

		$columns    = array();
		$entry_meta = self::get_entry_meta( $form_id );
		foreach ( $field_ids as $field_id ) {

			switch ( $field_id ) {
				case 'id' :
					$columns[ $field_id ] = array( 'label' => esc_html__( 'Entry Id', 'gravityforms' ), 'type' => 'id' );
					break;
				case 'ip' :
					$columns[ $field_id ] = array( 'label' => esc_html__( 'User IP', 'gravityforms' ), 'type' => 'ip' );
					break;
				case 'date_created' :
					$columns[ $field_id ] = array( 'label' => esc_html__( 'Entry Date', 'gravityforms' ), 'type' => 'date_created' );
					break;
				case 'source_url' :
					$columns[ $field_id ] = array( 'label' => esc_html__( 'Source Url', 'gravityforms' ), 'type' => 'source_url' );
					break;
				case 'payment_status' :
					$columns[ $field_id ] = array( 'label' => esc_html__( 'Payment Status', 'gravityforms' ), 'type' => 'payment_status' );
					break;
				case 'transaction_id' :
					$columns[ $field_id ] = array( 'label' => esc_html__( 'Transaction Id', 'gravityforms' ), 'type' => 'transaction_id' );
					break;
				case 'payment_date' :
					$columns[ $field_id ] = array( 'label' => esc_html__( 'Payment Date', 'gravityforms' ), 'type' => 'payment_date' );
					break;
				case 'payment_amount' :
					$columns[ $field_id ] = array( 'label' => esc_html__( 'Payment Amount', 'gravityforms' ), 'type' => 'payment_amount' );
					break;
				case 'created_by' :
					$columns[ $field_id ] = array( 'label' => esc_html__( 'User', 'gravityforms' ), 'type' => 'created_by' );
					break;
				case ( ( is_string( $field_id ) || is_int( $field_id ) ) && array_key_exists( $field_id, $entry_meta ) ) :
					$columns[ $field_id ] = array( 'label' => $entry_meta[ $field_id ]['label'], 'type' => $field_id );
					break;
				default :
					$field = self::get_field( $form, $field_id );
					if ( $field ) {
						if ( $field->type === 'consent' ) {
							if ( false !== strpos( $field_id, '.1' ) ) {
								$columns[ strval( $field_id ) ] = array( 'label'     => self::get_label( $field, $field_id, false ),
								                                         'type'      => $field->type,
								                                         'inputType' => $field->inputType
								);
							}
						} else {
							$input_label_only               = apply_filters( 'gform_entry_list_column_input_label_only', $input_label_only, $form, $field );
							$columns[ strval( $field_id ) ] = array( 'label'     => self::get_label( $field, $field_id, $input_label_only ),
							                                         'type'      => $field->type,
							                                         'inputType' => $field->inputType
							);
						}
					}
			}
		}

		return $columns;
	}

	/**
	 * @param GF_Field $field
	 * @param int $input_id
	 * @param bool $input_only
	 *
	 * @return string
	 */
	public static function get_label( $field, $input_id = 0, $input_only = false, $allow_admin_label = true ) {
		if ( ! $field instanceof GF_Field ) {
			$field = GF_Fields::create( $field );
		}

		$field_label = ( GFForms::get_page() ||
		                 RG_CURRENT_PAGE == 'select_columns.php' ||
		                 RG_CURRENT_PAGE == 'print-entry.php' ||
		                 rgget( 'gf_page', $_GET ) == 'select_columns' || // phpcs:ignore WordPress.Security.NonceVerification.Recommended
		                 rgget( 'gf_page', $_GET ) == 'print-entry' ||  // phpcs:ignore WordPress.Security.NonceVerification.Recommended
		                 $field->get_context_property( 'use_admin_label' )
		               ) && ! empty( $field->adminLabel ) && $allow_admin_label ? $field->adminLabel : $field->label;

		$input = self::get_input( $field, $input_id );

		if ( self::get_input_type( $field ) == 'checkbox' && $input != null ) {
			return $input['label'];
		} else if ( $input != null ) {
			if ( self::get_input_type( $field ) === 'consent' &&
			     ( RG_CURRENT_PAGE == 'select_columns.php' ||
			       RG_CURRENT_PAGE == 'print-entry.php' ||
			       rgget( 'gf_page', $_GET ) == 'select_columns' || // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			       rgget( 'gf_page', $_GET ) == 'print-entry' || // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			       GFForms::get_page() === 'entry_list'
			     ) ) {
				return $field_label;
			}

			$input_label = rgar( $input, 'customLabel', rgar( $input, 'label' ) );

			return $input_only ? $input_label : $field_label . ' (' . $input_label . ')';
		} else {
			return $field_label;
		}
	}

	/**
	 * Get the text that tells the user that the field is required.
	 *
	 * @since 2.5
	 *
	 * @param $form_id
	 *
	 * @return string HTML required indicator.
	 */
	public static function get_required_indicator( $form_id ) {
		$meta = self::get_form_meta( $form_id );
		$required_indicator = rgar( $meta, 'requiredIndicator' );

		switch( $required_indicator ) {
			case 'text':
				$indicator       = esc_html__( '(Required)', 'gravityforms' );
				$indicator_class = 'gfield_required_text';
				break;
			case 'asterisk':
				$indicator       = '*';
				$indicator_class = 'gfield_required_asterisk';
				break;
			case 'custom':
				$indicator       = rgar( $meta, 'customRequiredIndicator' ) ? $meta['customRequiredIndicator'] : esc_html__( '(Required)', 'gravityforms' );
				$indicator_class = 'gfield_required_custom';
				break;
			default:
				$legacy_markup   = GFCommon::is_legacy_markup_enabled( $meta );
				$indicator       = $legacy_markup ? '*' : esc_html__( '(Required)', 'gravityforms' );
				$indicator_class = $legacy_markup ? 'gfield_required_asterisk' : 'gfield_required_text';
				break;
		}

		return '<span class="gfield_required ' . $indicator_class . '">' . $indicator . '</span>';
	}

	/**
	 * @param GF_Field $field
	 * @param          $id
	 *
	 * @return null
	 */
	public static function get_input( $field, $id ) {
		if ( is_array( $field->inputs ) ) {
			foreach ( $field->inputs as $input ) {
				if ( $input['id'] == $id ) {
					return $input;
				}
			}
		}

		return null;
	}

	public static function has_input( $field, $input_id ) {
		if ( ! is_array( $field->inputs ) ) {
			return false;
		} else {
			foreach ( $field->inputs as $input ) {
				if ( $input['id'] == $input_id ) {
					return true;
				}
			}

			return false;
		}
	}

	/**
	 * Returns the URL of the current request.
	 *
	 * @since Unknown
	 * @since 2.9.1 Updated to return the referring URL for requests made via admin-ajax.php.
	 *
	 * @param bool $force_ssl Indicates if the URL should start with https.
	 *
	 * @return string
	 */
	public static function get_current_page_url( $force_ssl = false ) {
		$pageURL = 'http';
		if ( rgar( $_SERVER, 'HTTPS' ) == 'on' || $force_ssl ) {
			$pageURL .= 's';
		}
		$pageURL .= '://' . rgar( $_SERVER, 'HTTP_HOST' ) . rgar( $_SERVER, 'REQUEST_URI' );

		if ( strpos( $pageURL, admin_url( 'admin-ajax.php' ) ) === 0 ) {
			return wp_get_referer();
		}

		return $pageURL;
	}

	public static function get_submitted_fields( $form_id ) {
		global $wpdb;

		if ( version_compare( self::get_database_version(), '2.3-dev-1', '<' ) ) {
			return GF_Forms_Model_Legacy::get_submitted_fields( $form_id );
		}

		$entry_meta_table_name = self::get_entry_meta_table_name();
		$field_list             = '';
		$fields                 = $wpdb->get_col( $wpdb->prepare( "SELECT DISTINCT meta_key FROM $entry_meta_table_name WHERE form_id=%d AND meta_key REGEXP '^[0-9]+(\.[0-9]+)?$'", $form_id ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$field_list             = implode( ',', array_unique( array_map( 'intval', $fields ) ) );

		return $field_list;
	}

	/**
	 * Returns the field object for the requested field or input ID from the supplied or specified form.
	 *
	 * @since  2.3 Updated to support being passed the form id or form object as the first parameter.
	 * @since  unknown.
	 * @access public
	 *
	 * @param array|int  $form_or_id The Form Object or ID.
	 * @param string|int $field_id   The field or input ID.
	 *
	 * @return GF_Field|null
	 */
	public static function get_field( $form_or_id, $field_id ) {
		$form = is_numeric( $form_or_id ) ? self::get_form_meta( $form_or_id ) : $form_or_id;

		if ( ! isset( $form['fields'] ) || ! isset( $form['id'] ) || ! is_array( $form['fields'] ) || empty( $field_id ) ) {
			return null;
		}

		if ( is_numeric( $field_id ) || preg_match( '/^\d+\.\w+$/', $field_id ) ) {
			// Removing the input-specific segment from the field ID (i.e 1.3 or 1.something -> 1).
			$field_id = intval( $field_id );
		}

		global $_fields;
		$key = $form['id'] . '_' . $field_id;
		$return = null;
		if ( isset( $_fields[ $key ] ) ) {
			return $_fields[ $key ];
		}

		$_fields[ $key ] = null;
		foreach ( $form['fields'] as $field ) {
			if ( $field->id == $field_id ) {
				$_fields[ $key ] = $field;
				$return          = $field;
				break;
			} elseif ( is_array( $field->fields ) ) {
				$sub_field = self::get_sub_field( $field, $field_id );
				if ( $sub_field ) {
					$_fields[ $key ] = $sub_field;
					return $sub_field;
				}

			}
		}

		return $return;
	}

	/**
	 * Returns the field inside a repeater field with the specified ID.
	 *
	 * @since 2.4
	 *
	 * @param GF_Field_Repeater $repeater_field The repeater field.
	 * @param int $field_id                     The field ID.
	 *
	 * @return null|GF_Field
	 */
	public static function get_sub_field( $repeater_field, $field_id ) {
		if ( is_array( $repeater_field->fields ) ) {
			foreach ( $repeater_field->fields as $field ) {
				if ( $field->id == $field_id ) {
					return $field;
				} elseif ( is_array( $field->fields ) ) {
					$f = self::get_sub_field( $field, $field_id );
					if ( $f ) {
						return $f;
					}
				}
			}
		}
		return null;
	}

	/**
	 * @deprecated 2.8 HTML5 setting was removed, and HTML5 is now always enabled.
	 * @remove-in 3.0
	 *
	 * @return true
	 */
	public static function is_html5_enabled() {
		_deprecated_function( __METHOD__ , '2.8' );
		return true;
	}

	/**
	 * Return the current lead being processed. Should only be called when a form has been submitted.
	 * If called before the "real" lead has been saved to the database, uses self::create_lead() to create
	 * a temporary lead to work with.
	 *
	 * @since Unknown
	 * @since 2.7.1 Added the optional $form arg.
	 *
	 * @param array $form The form being processed or an empty array to get the form based on the ID from the gform_submit input.
	 *
	 * @return false|array
	 */
	public static function get_current_lead( $form = array() ) {
		$form_id = absint( rgpost( 'gform_submit' ) );

		// If a GF submission is not in process, always return false.
		if ( empty( $form_id ) ) {
			return false;
		}

		if ( rgar( self::$_current_lead, 'form_id' ) != $form_id ) {
			if ( absint( rgar( $form, 'id' ) ) !== $form_id ) {
				$form = self::get_form_meta( $form_id );
			}
			self::$_current_lead = self::create_lead( $form );
		}

		return self::$_current_lead;
	}

	/**
	 * Populates the the static $_current_lead property withh the given entry, for use in hooks where it is not explicitly passed.
	 *
	 * @since unknown
	 * @since 2.9.16 Added the $flush_cache param.
	 *
	 * @param array $lead        The entry to be cached.
	 * @param bool  $flush_cache Indicates if the contents of the static $_cache property should be flushed.
	 */
	public static function set_current_lead( $lead, $flush_cache = true ) {
		if ( $flush_cache ) {
			GFCache::flush();
		}
		self::$_current_lead = $lead;
	}

	/**
	 * Converts the legacy confirmation from forms created prior to v1.7 to the current format or adds the default confirmation.
	 *
	 * @since 1.7
	 * @since 2.4.15 Fixed corrupt confirmation being created when form doesn't have one to convert.
	 *
	 * @param array $form The form being processed.
	 *
	 * @return array
	 */
	public static function convert_confirmation( $form ) {
		$confirmation = rgar( $form, 'confirmation' );

		if ( ! empty( $confirmation['type'] ) ) {
			// Complete converting legacy confirmation by adding missing properties.
			$confirmation['id']        = uniqid();
			$confirmation['name']      = esc_html__( 'Default Confirmation', 'gravityforms' );
			$confirmation['isDefault'] = true;
		} else {
			// Form does not have a valid legacy confirmation add the default confirmation instead.
			$confirmation = self::get_default_confirmation();
		}

		$form['confirmations'] = array( $confirmation['id'] => $confirmation );

		self::save_form_confirmations( $form['id'], $form['confirmations'] );

		return $form;
	}

	/**
	 * Returns a default confirmation.
	 *
	 * @since 2.4.15
	 * @since 2.10.0 Updated to include the default spam confirmation.
	 *
	 * @param string $event The confirmation event. form_saved, form_save_email_sent, spam, or an empty string for the default form submission event.
	 *
	 * @return array
	 */
	public static function get_default_confirmation( $event = '' ) {
		switch ( $event ) {
			case 'form_saved':
				return array(
					'id'          => uniqid( 'sc1' ),
					'event'       => 'form_saved',
					'name'        => __( 'Save and Continue Confirmation', 'gravityforms' ),
					'isDefault'   => true,
					'type'        => 'message',
					'message'     => sprintf(
						'<h2>%s</h2><p role="alert">%s</p><p class="resume_form_link_wrapper">{save_link}</p><p>%s<br />%s</p>{save_email_input}',
						__( 'Link to continue editing later', 'gravityforms' ),
						__( 'Please use the following link to return and complete this form from any computer.', 'gravityforms' ),
						__( 'Note: This link will expire after 30 days.', 'gravityforms' ),
						__( 'Enter your email address if you would like to receive the link via email.', 'gravityforms' )
					),
					'url'         => '',
					'pageId'      => '',
					'queryString' => '',
				);

			case 'form_save_email_sent':
				return array(
					'id'          => uniqid( 'sc2' ),
					'event'       => 'form_save_email_sent',
					'name'        => __( 'Save and Continue Email Sent Confirmation', 'gravityforms' ),
					'isDefault'   => true,
					'type'        => 'message',
					'message'     => sprintf(
						'<h2 class="saved_message_success">%s</h2><p>%s <span class="saved_message_email">{save_email}</span></p>',
						__( 'Success!', 'gravityforms' ),
						__( 'The link was sent to the following email address:', 'gravityforms' )
					),
					'url'         => '',
					'pageId'      => '',
					'queryString' => '',
				);

			case 'spam':
				$confirmation          = self::get_default_confirmation();
				$confirmation['name']  = __( 'Spam Confirmation', 'gravityforms' );
				$confirmation['event'] = 'spam';

				return $confirmation;
			default:
				return array(
					'id'          => uniqid(),
					'name'        => __( 'Default Confirmation', 'gravityforms' ),
					'isDefault'   => true,
					'type'        => 'message',
					'message'     => __( 'Thanks for contacting us! We will get in touch with you shortly.', 'gravityforms' ),
					'url'         => '',
					'pageId'      => '',
					'queryString' => '',
				);
		}
	}

	public static function load_confirmations( $form ) {

		$confirmations = self::get_form_confirmations( $form['id'] );

		// If there are no confirmations convert the legacy confirmation or add the default.
		if ( empty( $confirmations ) ) {
			$form = self::convert_confirmation( $form );
		} else {
			$form['confirmations'] = $confirmations;
		}

		return $form;
	}

	public static function get_form_confirmations( $form_id ) {
		global $wpdb;

		$key = self::get_form_cache_key( $form_id );

		if ( isset( self::$_confirmations[ $key ] ) ) {
			return self::$_confirmations[ $key ];
		}

		$tablename     = GFFormsModel::get_meta_table_name();
		$sql           = $wpdb->prepare( "SELECT confirmations FROM %i WHERE form_id = %d",  $tablename, $form_id );
		$results       = $wpdb->get_results( $sql, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$confirmations = rgars( $results, '0/confirmations', array() );

		if ( ! empty( $confirmations ) ) {
			$confirmations = self::remove_corrupt_confirmations( self::unserialize( $confirmations ) );
		}

		self::$_confirmations[ $key ] = $confirmations;

		return self::$_confirmations[ $key ];
	}

	/**
	 * Delete a form confirmation by ID.
	 *
	 * @since  2.5
	 *
	 * @param string    $confirmation_id The confirmation to be deleted.
	 * @param int|array $form_id         The form ID or Form Object form the confirmation being deleted.
	 *
	 * @return false|int The result of the database operation.
	 */
	public static function delete_form_confirmation( $confirmation_id, $form_id ) {

		// If no Form ID or object was provided, exit.
		if ( ! $form_id ) {
			return false;
		}

		// Get Form object if only ID was provided.
		$form = ! is_array( $form_id ) ? self::get_form_meta( $form_id ) : $form_id;

		/**
		 * Fires right before a confirmation is deleted.
		 *
		 * @since 1.9
		 *
		 * @param int   $form ['confirmations'][$confirmation_id] The ID of the confirmation being deleted.
		 * @param array $form The Form object.
		 */
		do_action( 'gform_pre_confirmation_deleted', $form['confirmations'][ $confirmation_id ], $form );

		unset( $form['confirmations'][ $confirmation_id ] );

		// Clear form cache so next retrieval of form meta will reflect deleted confirmation.
		self::flush_current_form( self::get_form_cache_key( $form_id ) );

		return self::save_form_confirmations( $form['id'], $form['confirmations'] );

	}

	/**
	 * Remove corrupt confirmations created by old versions of GFFormsModel::convert_confirmation().
	 *
	 * @since 2.4.15
	 *
	 * @param mixed $confirmations The confirmations to be processed.
	 *
	 * @return array
	 */
	public static function remove_corrupt_confirmations( $confirmations ) {
		if ( ! is_array( $confirmations ) ) {
			return array();
		}

		foreach ( $confirmations as $id => $confirmation ) {
			if ( is_array( $confirmation ) && ! empty( $confirmation['type'] ) ) {
				continue;
			}

			unset( $confirmations[ $id ] );
		}

		return $confirmations;
	}

	public static function save_form_confirmations( $form_id, $confirmations ) {
		return self::update_form_meta( $form_id, $confirmations, 'confirmations' );
	}

	public static function save_form_notifications( $form_id, $notifications ) {
		return self::update_form_meta( $form_id, $notifications, 'notifications' );
	}

	/**
	 * Returns the ids of the specified forms.
	 *
	 * @since unknown
	 * @since 2.5     Added $sort_column and $sort_dir parameters.
	 * @since 2.5.8   Added support for passing null for the $active and $trash args.
	 *
	 * @param bool|null $active      True if active forms are returned. False to get inactive forms. Null to ignore the is_active property. Defaults to true.
	 * @param bool|null $trash       True if trashed forms are returned. False to exclude trash. Null to ignore the is_trash property. Defaults to false.
	 * @param string    $sort_column The column to sort the results on.
	 * @param string    $sort_dir    The sort direction, ASC or DESC.
	 *
	 * @return array of form IDs.
	 */
	public static function get_form_ids( $active = true, $trash = false, $sort_column = 'id', $sort_dir = 'ASC' ) {
		global $wpdb;

		$sql   = 'SELECT id FROM ' . self::get_form_table_name();
		$where = array();

		if ( null !== $active ) {
			$where[] = $wpdb->prepare( 'is_active=%d', $active );
		}

		if ( null !== $trash ) {
			$where[] = $wpdb->prepare( 'is_trash=%d', $trash );
		}

		if ( ! empty( $where ) ) {
			$sql .= ' WHERE ' . join( ' AND ', $where );
		}

		if ( ! in_array( strtolower( $sort_column ), GFFormsModel::get_form_db_columns() ) ) {
			$sort_column = 'id';
		}

		if ( ! empty( $sort_column ) ) {
			$sql .= " ORDER BY $sort_column " . ( $sort_dir == 'ASC' ? 'ASC' : 'DESC' );
		}

		return $wpdb->get_col( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
	}

	/**
	 * Get forms and return any form column(s).
	 *
	 * @see GFFormsModel::get_form_ids()
	 * @since 2.7.5
	 *
	 * @param bool   $active      True if active forms are returned. False to get inactive forms. Defaults to true.
	 * @param bool   $trash       True if trashed forms are returned. False to exclude trash. Defaults to false.
	 * @param string $sort_column The column to sort the results on.
	 * @param string $sort_dir    The sort direction, ASC or DESC.
	 * @param array  $columns     The columns to return. Defaults to ['id']. Other options are 'title', 'date_created', 'is_active', 'is_trash'. 'date_updated' may be supported, depending on the Gravity Forms version.
	 *
	 * @return array Forms indexed from 0 by SQL result row number. Each row is an associative array (column => value).
	 */
	public static function get_forms_columns( $active = true, $trash = false, $sort_column = 'id', $sort_dir = 'ASC', $columns = array( 'id' ) ) {
		global $wpdb;

		// Only allow valid columns.
		$columns = array_intersect( $columns, GFFormsModel::get_form_db_columns() );

		$sql   = 'SELECT ' . implode( ', ', $columns ) . ' FROM ' . GFFormsModel::get_form_table_name();
		$where = array();

		if ( null !== $active ) {
			$where[] = $wpdb->prepare( 'is_active=%d', $active );
		}

		if ( null !== $trash ) {
			$where[] = $wpdb->prepare( 'is_trash=%d', $trash );
		}

		if ( ! empty( $where ) ) {
			$sql .= ' WHERE ' . join( ' AND ', $where );
		}

		if ( ! in_array( strtolower( $sort_column ), GFFormsModel::get_form_db_columns() ) ) {
			$sort_column = 'id';
		}

		if ( ! empty( $sort_column ) ) {
			$sql .= " ORDER BY $sort_column " . ( $sort_dir == 'ASC' ? 'ASC' : 'DESC' );
		}

		return $wpdb->get_results( $sql, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
	}

	public static function get_entry_meta( $form_ids ) {
		global $_entry_meta;

		if ( $form_ids == 0 ) {
			$form_ids = self::get_form_ids();
		}

		if ( ! is_array( $form_ids ) ) {
			$form_ids = array( $form_ids );
		}
		$meta = array();
		foreach ( $form_ids as $form_id ) {
			if ( ! isset( $_entry_meta[ $form_id ] ) ) {
				$_entry_meta           = array();
				$_entry_meta[ $form_id ] = apply_filters( 'gform_entry_meta', array(), $form_id );
			}
			$meta = array_merge( $meta, $_entry_meta[ $form_id ] );
		}

		return $meta;
	}

	public static function set_entry_meta( $lead, $form ) {
		GFCommon::timer_start( __METHOD__ );
		$entry_id   = absint( rgar( $lead, 'id' ) );
		$entry_meta = self::get_entry_meta( $form['id'] );
		$keys       = array_keys( $entry_meta );
		foreach ( $keys as $key ) {
			if ( isset( $entry_meta[ $key ]['update_entry_meta_callback'] ) ) {
				$callback = $entry_meta[ $key ]['update_entry_meta_callback'];
				$value    = call_user_func_array( $callback, array( $key, $lead, $form ) );
				gform_update_meta( $entry_id, $key, $value );
				$lead[ $key ] = $value;
			}
		}

		GFCommon::log_debug( __METHOD__ . sprintf( '(): Saving meta for entry (#%d) completed in %F seconds.', $entry_id, GFCommon::timer_end( __METHOD__ ) ) );

		return $lead;
	}

	/**
	 *
	 * @param $form_id
	 * @param array $search_criteria
	 * @param null $sorting
	 * @param null $paging
	 *
	 * @return array
	 */
	public static function search_leads( $form_id, $search_criteria = array(), $sorting = null, $paging = null ) {
		return GFAPI::get_entries( $form_id, $search_criteria, $sorting, $paging );
	}


	public static function search_lead_ids( $form_id, $search_criteria = array() ) {
		return GFAPI::get_entry_ids( $form_id, $search_criteria );
	}

	/**
	 * Returns the gf_entry table field names.
	 *
	 * @since 2.3.2.13 Added date_updated.
	 * @since unknown
	 *
	 * @return array
	 */
	public static function get_lead_db_columns() {
		return array(
			'id',
			'form_id',
			'post_id',
			'date_created',
			'date_updated',
			'is_starred',
			'is_read',
			'ip',
			'source_url',
			'user_agent',
			'currency',
			'payment_status',
			'payment_date',
			'payment_amount',
			'payment_method',
			'transaction_id',
			'is_fulfilled',
			'created_by',
			'transaction_type',
			'status',
			'source_id',
		);
	}

	/**
	 *
	 * @param $form_id
	 * @param array $search_criteria
	 *
	 * @return null|string
	 */
	public static function count_search_leads( $form_id, $search_criteria = array() ) {
		return GFAPI::count_entries( $form_id, $search_criteria );
	}

	/**
	 * Returns the lead (entry) count for all forms.
	 *
	 * @param string $status
	 *
	 * @return null|string
	 */
	public static function get_lead_count_all_forms( $status = 'active' ) {
		return self::get_entry_count_all_forms( $status );
	}

	/**
	 * Returns the entry count for all forms.
	 *
	 * @param string $status
	 *
	 * @return null|string
	 */
	public static function get_entry_count_all_forms( $status = 'active' ) {
		global $wpdb;

		if ( version_compare( self::get_database_version(), '2.3-dev-1', '<' ) ) {
			return GF_Forms_Model_Legacy::get_lead_count_all_forms( $status );
		}

		$entry_table_name   = self::get_entry_table_name();

		if ( ! GFCommon::table_exists( $entry_table_name ) ) {
			return 0;
		}

		$sql = $wpdb->prepare( "SELECT count(id)
								FROM %i
								WHERE status=%s", $entry_table_name, $status );

		return $wpdb->get_var( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
	}

	public static function get_entry_meta_counts() {
		global $wpdb;


		if ( version_compare( self::get_database_version(), '2.3-dev-1', '<' ) ) {
			return GF_Forms_Model_Legacy::get_entry_meta_counts();
		}

		$meta_table_name = self::get_entry_meta_table_name();
		$notes_table_name = self::get_entry_notes_table_name();

		if ( ! GFCommon::table_exists( $meta_table_name ) ) {
			return array(
				'details' => 0,
				'meta'    => 0,
				'notes'   => 0,
			);
		}

		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$results = $wpdb->get_results(
			"
            SELECT
            (SELECT count(0) FROM $meta_table_name) as meta,
            (SELECT count(0) FROM $notes_table_name) as notes
            "
		);
		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

		return array(
			'details' => intval( $results[0]->meta ),
			'meta'    => intval( $results[0]->meta ),
			'notes'   => intval( $results[0]->notes ),
		);

	}

	/**
	 * @deprecated 2.2 Use gf_upgrade()->dbDelta() instead
	 * @remove-in 3.0
	 */
	public static function dbDelta( $sql ) {
		_deprecated_function( 'dbDelta', '2.2', 'gf_upgrade()->dbDelta()' );

		gf_upgrade()->dbDelta( $sql );

	}

	public static function get_db_charset() {
		global $wpdb;

		if ( ! empty( $wpdb->charset ) ) {
			$charset_collate = "DEFAULT CHARACTER SET $wpdb->charset";
		}

		if ( ! empty( $wpdb->collate ) ) {
			$charset_collate .= " COLLATE $wpdb->collate";
		}

		return $charset_collate;
	}

	public static function is_valid_table( $table_name ){
		global $wpdb;

		$tables = array(
			GFFormsModel::get_form_table_name(),
			GFFormsModel::get_form_view_table_name(),
			GFFormsModel::get_meta_table_name(),
			GFFormsModel::get_lead_table_name(),
			GFFormsModel::get_lead_notes_table_name(),
			GFFormsModel::get_lead_details_table_name(),
			GFFormsModel::get_lead_details_long_table_name(),
			GFFormsModel::get_lead_meta_table_name(),
			GFFormsModel::get_incomplete_submissions_table_name(),
			self::get_addon_feed_table_name(),
			"{$wpdb->prefix}gf_addon_payment_transaction",
			"{$wpdb->prefix}gf_addon_payment_callback",

			GFFormsModel::get_entry_table_name(),
			GFFormsModel::get_entry_notes_table_name(),
			GFFormsModel::get_entry_meta_table_name(),
			GFFormsModel::get_draft_submissions_table_name(),
		);

		return in_array( $table_name, $tables );
	}

	public static function is_valid_index( $index_name ){

		$indexes = array(
			'id',
			'form_id',
			'status',
			'lead_id',
			'lead_user_key',
			'lead_field_number',
			'lead_detail_id',
			'lead_detail_key',
			'meta_key',
			'form_id_meta_key',
			'uuid',
			'transaction_type',
			'type_lead',
			'slug_callback_id',
			'addon_slug_callback_id',
			'addon_form',
		);

		return in_array( $index_name, $indexes );
	}

	/**
	 * Trims values inside choice texts, choice values, input labels, field labels and field conditionalLogic
	 *
	 * @param array $form         Form object.
	 * @param bool  $form_updated Output parameter.
	 *
	 * @return array $form
	 */
	public static function trim_form_meta_values( $form, &$form_updated = false ) {
		$form_id = $form['id'];
		GFCommon::log_debug( 'GFFormsModel::trim_form_meta_values(): Starting.' );
		if ( isset( $form['fields'] ) && is_array( $form['fields'] ) ) {
			foreach ( $form['fields'] as &$field ) {
				$trim_value = apply_filters( 'gform_trim_input_value', true, $form_id, $field );
				if ( ! $trim_value ) {
					continue;
				}

				if ( isset( $field->label ) && $field->label != trim( $field->label ) ) {
					$field->label = trim( $field->label );
					$form_updated = true;
				}
				if ( is_array( $field->choices ) ) {
					foreach ( $field->choices as &$choice ) {
						if ( isset( $choice['text'] ) && $choice['text'] != trim( $choice['text'] ) ) {
							$choice['text'] = trim( $choice['text'] );
							$form_updated   = true;
						}
						if ( isset( $choice['value'] ) && $choice['value'] != trim( $choice['value'] ) ) {
							$choice['value'] = trim( $choice['value'] );
							$form_updated    = true;
						}
					}
				}
				if ( is_array( $field->inputs ) ) {
					foreach ( $field->inputs as &$input ) {
						if ( isset( $input['label'] ) && $input['label'] != trim( $input['label'] ) ) {
							$input['label'] = trim( $input['label'] );
							$form_updated   = true;
						}
					}
				}
			}
			$form['fields'] = GFFormsModel::trim_conditional_logic_values( $form['fields'], $form, $form_updated );
		}
		if ( $form_updated ) {
			GFCommon::log_debug( 'GFFormsModel::trim_form_meta_values(): Form values trimmed.' );
		}

		return $form;
	}

	/**
	 * Trims values from an array of elements e.g. notifications and confirmations
	 *
	 * @param array $meta_array Form object.
	 * @param array $form       Form object.
	 * @param bool  $updated    Output parameter.
	 *
	 * @return array $meta_array
	 */
	public static function trim_conditional_logic_values( $meta_array, $form, &$updated = false ) {
		GFCommon::log_debug( 'GFFormsModel::trim_conditional_logic_values(): Starting.' );
		if ( is_array( $meta_array ) ) {
			foreach ( $meta_array as &$meta ) {
				$meta = self::trim_conditional_logic_values_from_element( $meta, $form, $updated );
			}
		}
		if ( $updated ) {
			GFCommon::log_debug( 'GFFormsModel::trim_conditional_logic_values(): Conditional logic values trimmed.' );
		}

		return $meta_array;
	}

	/**
	 * Trims values from elements e.g. fields, notifications and confirmations
	 *
	 * @param array $element Form object.
	 * @param array $form    Form object.
	 * @param bool  $updated Output parameter.
	 *
	 * @return array $element
	 */
	public static function trim_conditional_logic_values_from_element( $element, $form = array(), &$updated = false ) {
		if ( $element instanceof GF_Field ) {

			/* @var GF_Field $element */
			if ( is_array( $element->conditionalLogic ) && isset( $element->conditionalLogic['rules'] ) && is_array( $element->conditionalLogic['rules'] ) ) {
				foreach ( $element->conditionalLogic['rules'] as &$rule ) {
					$value = (string) $rule['value'];
					if ( $value !== trim( $value ) ) {
						$field      = isset( $form['fields'] ) ? GFFormsModel::get_field( $form, $rule['fieldId'] ) : array();
						$trim_value = apply_filters( 'gform_trim_input_value', true, rgar( $form, 'id' ), $field );
						if ( $trim_value ) {
							$rule['value'] = trim( $rule['value'] );
							$updated       = true;
						}
					}
				}
			}
		} else {
			if ( isset( $element['conditionalLogic'] ) && is_array( $element['conditionalLogic'] ) && isset( $element['conditionalLogic']['rules'] ) && is_array( $element['conditionalLogic']['rules'] ) ) {
				foreach ( $element['conditionalLogic']['rules'] as &$rule ) {
					$value = (string) rgar( $rule, 'value' );
					if ( $value !== trim( $value ) ) {
						$field      = isset( $form['fields'] ) ? GFFormsModel::get_field( $form, $rule['fieldId'] ) : array();
						$trim_value = apply_filters( 'gform_trim_input_value', true, rgar( $form, 'id' ), $field );
						if ( $trim_value ) {
							$rule['value'] = trim( $rule['value'] );
							$updated       = true;
						}
					}
				}
			}
		}

		return $element;
	}

	/**
	 * Returns an array of field IDs that have been encrypted using GFCommon::encrypt()
	 *
	 * @deprecated
	 * @remove-in 3.0
	 *
	 * @since unknown
	 *
	 * @param $entry_id
	 *
	 * @return array|bool|mixed
	 */
	public static function get_encrypted_fields( $entry_id ) {

		_deprecated_function( 'GFCommon:get_encrypted_fields', '2.3', 'GFCommon:get_openssl_encrypted_fields' );

		$encrypted_fields = gform_get_meta( $entry_id, '_encrypted_fields' );

		if ( empty( $encrypted_fields ) ) {
			$encrypted_fields = array();
		}

		return $encrypted_fields;
	}

	/**
	 * Stores the field IDs that have been encrypted using GFCommon::encrypt()
	 *
	 * @deprecated
	 * @remove-in 3.0
	 *
	 * @since unknown
	 *
	 * @param $entry_id
	 * @param $field_ids
	 *
	 * @return bool
	 */
	public static function set_encrypted_fields( $entry_id, $field_ids ) {

		_deprecated_function( 'GFCommon:set_encrypted_fields', '2.3', 'GFCommon:set_openssl_encrypted_fields' );

		if ( ! is_array( $field_ids ) ) {
			$field_ids = array( $field_ids );
		}

		$encrypted_fields = array_merge( self::get_encrypted_fields( $entry_id ), $field_ids );

		gform_update_meta( $entry_id, '_encrypted_fields', $encrypted_fields );

		return true;
	}

	/**
	 * Checks whether the given field was encrypted using GFCommon::encrpyt() and registered using GFCommon::set_encrypted_fields()
	 *
	 * @deprecated
	 * @remove-in 3.0
	 *
	 * @since unknown
	 *
	 * @param $entry_id
	 * @param $field_id
	 *
	 * @return bool|mixed|void
	 */
	public static function is_encrypted_field( $entry_id, $field_id ) {

		_deprecated_function( 'GFCommon:is_encrypted_field', '2.3', 'GFCommon:is_openssl_encrypted_field' );

		/**
		 * Determines if an entry field is stored encrypted. Use this hook to change the default behavior of decrypting fields that have been encrypted or to completely disable the
		 * process if checking for encrypted fields.
		 *
		 * @param int $entry_id The current Entry ID
		 * @param int $field_id The current Field ID.
		 */
		$is_encrypted = apply_filters('gform_is_encrypted_field', '', $entry_id, $field_id );
		if (  $is_encrypted !== '' ){
			return $is_encrypted;
		}

		$encrypted_fields = self::get_encrypted_fields( $entry_id );

		return in_array( $field_id, $encrypted_fields );
	}

	/**
	 * Returns an array of field IDs that have been encrypted using GFCommon::openssl_encrypt()
	 *
	 * @since 2.3
	 *
	 * @param $entry_id
	 *
	 * @return array|bool|mixed
	 */
	public static function get_openssl_encrypted_fields( $entry_id ) {

		$encrypted_fields = gform_get_meta( $entry_id, '_openssl_encrypted_fields' );

		if ( empty( $encrypted_fields ) ) {
			$encrypted_fields = array();
		}

		return $encrypted_fields;
	}

	/**
	 * Returns the encrypted field IDs for each entry_id specified in the $entry_ids array.
	 *
	 * @since 2.6
	 *
	 * @param array $entry_ids An array of entry IDs.
	 *
	 * @return array Returns an array with entry_id as the key and an array of field IDs (that have been encrypted using GFCommon::openssl_encrypt()) as the value.
	 */
	public static function get_openssl_encrypted_fields_by_entries( $entry_ids ) {
		global $wpdb;

		$entry_ids        = array_unique( $entry_ids );
		$placeholders     = implode( ',', array_fill( 0, count( $entry_ids ), '%d' ) );
		$entry_meta_table = self::get_entry_meta_table_name();

		$sql     = sprintf( "SELECT entry_id, meta_value from $entry_meta_table WHERE meta_key = '_openssl_encrypted_fields' AND entry_id IN(%s)", $placeholders );
		$results = $wpdb->get_results( $wpdb->prepare( $sql, $entry_ids ), ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

		$encrypted_fields = array();
		foreach ( $results as $row ) {
			$encrypted_fields[ $row['entry_id'] ] = maybe_unserialize( $row['meta_value'] );
		}

		return $encrypted_fields;
	}

	/**
	 * Adds the field IDs that have been encrypted using GFCommon::encrypt(). Merges the new IDs with the existing IDs.
	 *
	 * @since 2.3
	 *
	 * @param $entry_id
	 * @param $field_ids
	 *
	 * @return bool
	 */
	public static function set_openssl_encrypted_fields( $entry_id, $field_ids ) {

		if ( ! is_array( $field_ids ) ) {
			$field_ids = array( $field_ids );
		}

		$encrypted_fields = array_merge( self::get_openssl_encrypted_fields( $entry_id ), $field_ids );

		gform_update_meta( $entry_id, '_openssl_encrypted_fields', $encrypted_fields );

		return true;
	}

	/**
	 * Checks whether the given field was encrypted using GFCommon::encrpyt() and registered using GFCommon::set_encrypted_fields()
	 *
	 * @since 2.3
	 *
	 * @param $entry_id
	 * @param $field_id
	 *
	 * @return bool|mixed|void
	 */
	public static function is_openssl_encrypted_field( $entry_id, $field_id ) {

		/**
		 * Determines if an entry field is stored encrypted. Use this hook to change the default behavior of decrypting fields that have been encrypted or to completely disable the
		 * process if checking for encrypted fields.
		 *
		 * @param int $entry_id The current Entry ID
		 * @param int $field_id The current Field ID.
		 */
		$is_encrypted = apply_filters('gform_is_encrypted_field', '', $entry_id, $field_id );
		if (  $is_encrypted !== '' ){
			return $is_encrypted;
		}

		$encrypted_fields = self::get_openssl_encrypted_fields( $entry_id );

		return in_array( $field_id, $encrypted_fields );
	}


	/**
	 * @deprecated 2.4.16
	 * @remove-in 3.0
	 *
	 * @param $entry
	 * @param $form
	 *
	 * @return mixed
	 */
	public static function delete_password( $entry, $form ) {
		_deprecated_function( __FUNCTION__, '2.4.16' );
		$password_fields = self::get_fields_by_type( $form, array( 'password' ) );
		if ( is_array( $password_fields ) ) {
			foreach ( $password_fields as $password_field ) {
				$entry[ $password_field->id ] = '';
			}
		}
		GFAPI::update_entry( $entry );

		return $entry;
	}

	public static function maybe_sanitize_form_settings( $form ) {
		if (  isset( $form['version'] ) && version_compare( $form['version'], '1.9.6.10', '>=' ) ) {
			$form = self::sanitize_settings( $form );
		}
		return $form;
	}

	public static function sanitize_settings( $form ) {

		$form['version'] = GFForms::$version;

		if ( apply_filters( 'gform_disable_form_settings_sanitization', false ) ) {
			return $form;
		}

		// -- standard form settings --

		$form['title'] = sanitize_text_field( rgar( $form, 'title' ) );
		if ( isset( $form['description'] ) ) {
			$form['description'] = self::maybe_wp_kses( $form['description'] );
		}

		if ( isset( $form['labelPlacement'] ) ) {
			$form['labelPlacement'] = GFCommon::whitelist( $form['labelPlacement'], array( 'top_label', 'left_label', 'right_label' ) );
		}

		if ( isset( $form['descriptionPlacement'] ) ) {
			$form['descriptionPlacement'] = GFCommon::whitelist( $form['descriptionPlacement'], array( 'below', 'above' ) );
		}

		if ( isset( $form['validationPlacement'] ) ) {
			$form['validationPlacement'] = GFCommon::whitelist( $form['validationPlacement'], array( 'below', 'above' ) );
		}

		if ( isset( $form['subLabelPlacement'] ) ) {
			$form['subLabelPlacement']    = GFCommon::whitelist( $form['subLabelPlacement'], array( 'below', 'above' ) );
		}

		// -- advanced form settings --

		if ( isset( $form['cssClass'] ) ) {
			$form['cssClass'] = sanitize_text_field( $form['cssClass'] );
		}

		if ( isset( $form['enableHoneypot'] ) ) {
			$form['enableHoneypot'] = (bool) $form['enableHoneypot'];
		}

		if ( isset( $form['honeypotAction'] ) ) {
			$form['honeypotAction']  = GFCommon::whitelist( $form['honeypotAction'], array( 'abort', 'spam' ) );
		}

		if ( isset( $form['enableAnimation'] ) ) {
			$form['enableAnimation'] = (bool) $form['enableAnimation'];
		}

		// form button settings
		if ( isset( $form['button'] ) ) {
			$form['button']['type']     = GFCommon::whitelist( $form['button']['type'], array( 'text', 'image' ) );
			$form['button']['text']     = $form['button']['type'] == 'text' ? sanitize_text_field( $form['button']['text'] ) : '';
			$form['button']['imageUrl'] = $form['button']['type'] == 'image' ? sanitize_text_field( $form['button']['imageUrl'] ) : '';
		}
		if ( isset( $form['button']['conditionalLogic'] ) ) {
			$form['button']['conditionalLogic'] = self::sanitize_conditional_logic( $form['button']['conditionalLogic'] );
		}

		// Save and Continue settings
		if ( isset( $form['save'] ) ) {
			$form['save']['enabled']        = (bool) $form['save']['enabled'] ;
			$form['save']['button']['type'] = 'link';
			$form['save']['button']['text'] = sanitize_text_field( $form['save']['button']['text'] );
		}


		// limit entries settings
		if ( isset( $form['limitEntries'] ) ) {
			$form['limitEntries']        = (bool) $form['limitEntries'];
			$form['limitEntriesCount']   = $form['limitEntries'] ? absint( $form['limitEntriesCount'] ) : '';
			$form['limitEntriesPeriod']  = $form['limitEntries'] ? GFCommon::whitelist( $form['limitEntriesPeriod'], array( '', 'day', 'week', 'month', 'year' ) ) : '';
			$form['limitEntriesMessage'] = $form['limitEntries'] ? self::maybe_wp_kses( $form['limitEntriesMessage'] ) : '';
		}

		// form scheduling settings
		if ( isset( $form['scheduleForm'] ) ) {
			$form['scheduleForm']           = (bool) $form['scheduleForm'];
			$form['scheduleStart']          = $form['scheduleForm'] ? wp_strip_all_tags( $form['scheduleStart'] ) : '';
			$form['scheduleStartHour']      = $form['scheduleForm'] ? GFCommon::int_range( $form['scheduleStartHour'], 1, 12 ) : '';
			$form['scheduleStartMinute']    = $form['scheduleForm'] ? GFCommon::int_range( $form['scheduleStartMinute'], 0, 59 ) : '';
			$form['scheduleStartAmpm']      = $form['scheduleForm'] ? GFCommon::whitelist( $form['scheduleStartAmpm'], array( 'am', 'pm' ) ) : '';
			$form['scheduleEnd']            = $form['scheduleForm'] ? wp_strip_all_tags( $form['scheduleEnd'] ) : '';
			$form['scheduleEndHour']        = $form['scheduleForm'] ? GFCommon::int_range( $form['scheduleEndHour'], 1, 12 ) : '';
			$form['scheduleEndMinute']      = $form['scheduleForm'] ? GFCommon::int_range( $form['scheduleEndMinute'], 0, 59 ) : '';
			$form['scheduleEndAmpm']        = $form['scheduleForm'] ? GFCommon::whitelist( $form['scheduleEndAmpm'], array( 'am', 'pm' ) ) : '';
			$form['schedulePendingMessage'] = $form['scheduleForm'] ? self::maybe_wp_kses( $form['schedulePendingMessage'] ) : '';
			$form['scheduleMessage']        = $form['scheduleForm'] ? self::maybe_wp_kses( $form['scheduleMessage'] ) : '';

		}

		// require login settings
		if ( isset( $form['requireLogin'] ) ) {
			$form['requireLogin']        = (bool) $form['requireLogin'];
			$form['requireLoginMessage'] = $form['requireLogin'] ? self::maybe_wp_kses( $form['requireLoginMessage'] ) : '';
		}

		if ( isset( $form['fields'] ) ) {
			foreach ( $form['fields'] as $field ) {
				/* @var GF_Field $field */
				$field->sanitize_settings();
			}
		}

		return $form;
	}

	public static function sanitize_conditional_logic( $logic ) {
		if ( ! is_array( $logic ) ) {
			return $logic;
		}

		if ( apply_filters( 'gform_disable_form_settings_sanitization', false ) ) {
			return $logic;
		}

		if ( isset( $logic['actionType'] ) && ! in_array( $logic['actionType'], array( 'show', 'hide' ) ) ) {
			$logic['actionType'] = 'show';
		}
		if (  isset( $logic['logicType'] ) && ! in_array( $logic['logicType'], array( 'all', 'any' ) ) ) {
			$logic['logicType'] = 'all';
		}

		if ( isset( $logic['rules'] ) && is_array( $logic['rules'] ) ) {
			foreach ( $logic['rules'] as &$rule ) {
				if ( isset( $rule['fieldId'] ) ) {
					// Field ID could be meta key.
					$rule['fieldId'] = wp_strip_all_tags( $rule['fieldId'] );
				}
				if ( isset( $rule['operator'] ) ) {
					$is_valid_operator = self::is_valid_operator( $rule['operator'] );
					$rule['operator'] = $is_valid_operator ? $rule['operator'] : 'is';
				}

				if ( isset( $rule['value'] ) ) {
					// Strip scripts but don't encode.
					$allowed_protocols = wp_allowed_protocols();
					$rule['value']     = wp_kses_no_null( $rule['value'], array( 'slash_zero' => 'keep' ) );
					$rule['value']     = wp_kses_hook( $rule['value'], 'post', $allowed_protocols );
					$rule['value']     = wp_kses_split( $rule['value'], 'post', $allowed_protocols );
				}
			}
		}
		return $logic;
	}

	private static function maybe_wp_kses( $html, $allowed_html = 'post', $allowed_protocols = array() ) {
		return GFCommon::maybe_wp_kses( $html, $allowed_html, $allowed_protocols );
	}

	/**
	 * Returns an array containing the form fields of the specified type or types.
	 *
	 * @since 1.9.9.10
	 * @param array $form
	 * @param array|string $types
	 * @param bool $use_input_type
	 *
	 * @return GF_Field[]
	 */
	public static function get_fields_by_type( $form, $types, $use_input_type = false ) {
		$fields = array();
		if ( ! is_array( rgar( $form, 'fields' ) ) ) {
			return $fields;
		}

		if ( ! is_array( $types ) ) {
			$types = array( $types );
		}

		foreach ( $form['fields'] as $field ) {
			/* @var GF_Field $field */
			$type = $use_input_type ? $field->get_input_type() : $field->type;
			if ( in_array( $type, $types ) ) {
				$fields[] = $field;
			}
		}

		return $fields;
	}

	/**
	 * Checks whether the conditional logic operator passed in is valid.
	 *
	 * @since  2.0.7.20 Refactored and added filter gform_is_valid_conditional_logic_operator.
	 * @access public
	 *
	 * @param string $operator Conditional logic operator.
	 *
	 * @return bool true if a valid operator, false if not.
	 */
	public static function is_valid_operator( $operator ) {
		$operators = array( 'is', 'isnot', '<>', 'not in', 'in', '>', '<', 'contains', 'starts_with', 'ends_with', 'like', '>=', '<=' );
		$is_valid = in_array( strtolower( $operator ), $operators );
		/**
		 * Filter which checks whether the operator is valid.
		 *
		 * Allows custom operators to be validated.
		 *
		 * @since 2.0.7.20
		 *
		 * @param bool   $is_valid Whether the operator is valid or not.
		 * @param string $operator The conditional logic operator.
		 */
		return apply_filters( 'gform_is_valid_conditional_logic_operator', $is_valid, $operator );
	}

	/**
	 * Update the recent forms list for the current user when a form is edited or trashed.
	 *
	 * @since 2.0.7.14
	 * @since 2.9.12 Removed the dependency on the admin toolbar being enabled.
	 *
	 * @param int $form_id The ID of the current form.
	 * @param bool $trashed Indicates if the form was trashed. Default is false, form was opened for editing.
	 */
	public static function update_recent_forms( $form_id, $trashed = false ) {
		$current_user_id = get_current_user_id();
		$recent_form_ids = self::get_recent_forms( $current_user_id );

		$i = array_search( $form_id, $recent_form_ids );

		if ( $i !== false ) {
			unset( $recent_form_ids[ $i ] );
			$recent_form_ids = array_values( $recent_form_ids );
		}

		if ( ! $trashed ) {
			// Add the current form to the top of the list.
			array_unshift( $recent_form_ids, $form_id );

			$recent_form_ids = array_slice( $recent_form_ids, 0, 10 );
		}

		update_user_meta( $current_user_id, 'gform_recent_forms', $recent_form_ids );
	}

	/**
	 * Get the recent forms list for the current user.
	 *
	 * @since 2.2.1.14
	 *
	 * @param int $current_user_id The ID of the currently logged in user.
	 *
	 * @return array
	 */
	public static function get_recent_forms( $current_user_id = 0 ) {
		if ( ! $current_user_id ) {
			$current_user_id = get_current_user_id();
		}

		$recent_form_ids = get_user_meta( $current_user_id, 'gform_recent_forms', true );

		if ( empty( $recent_form_ids ) || ! is_array( $recent_form_ids ) ) {
			$all_form_ids    = self::get_form_ids();
			$all_form_ids    = array_reverse( $all_form_ids );
			$recent_form_ids = array_slice( $all_form_ids, 0, 10 );
			if ( $recent_form_ids ) {
				update_user_meta( $current_user_id, 'gform_recent_forms', $recent_form_ids );
			}
		}

		return $recent_form_ids;
	}

	/**
	 * Evaluates the conditional logic based on the specified $logic variable. This method is used when evaluating field conditional logic.
	 * NOTE: There is a future refactoring opportunity to reduce code duplication by merging this method with GFCommon::evaluate_conditional_logic(), which currently handles non-field conditional logic (i.e. notification, confirmation and feed). A possible solution is for this method to call GFCommon::evaluate_conditional_logic().
	 *
	 * @param array $form         The current Form object.
	 * @param array $logic        The conditional logic configuration array with all the specified rules.
	 * @param array $field_values Default field values for this form. Used when form has not yet been submitted. Pass an array if no default field values are available/required.
	 * @param array $entry        Optional, default is null. If entry object is available, pass the entry.
	 *
	 * @return bool         Returns true if the conditional logic passes (i.e. field/button is supposed to be displayed), false otherwise.
	 */
	private static function evaluate_conditional_logic( $form, $logic, $field_values, $entry = null ) {

		if ( empty( $logic ) ){
			return true;
		}

		$match_count = 0;
		foreach ( $logic['rules'] as $rule ) {
			try {
				/**
				 * Filter the conditional logic rule before it is evaluated.
				 *
				 * @since 2.4.22
				 *
				 * @param array $rule         The conditional logic rule about to be evaluated.
				 * @param array $form         The current form meta.
				 * @param array $logic        All details required to evaluate an objects conditional logic.
				 * @param array $field_values The default field values for this form.
				 * @param array $entry        The current entry object (if available).
				 */
				$rule = apply_filters( 'gform_rule_pre_evaluation', $rule, $form, $logic, $field_values, $entry );
			} catch ( Error $e ) {
				GFCommon::log_error( __METHOD__ . '(): Error from function hooked to gform_rule_pre_evaluation. ' . $e->getMessage() );
			}
			$source_field   = RGFormsModel::get_field( $form, $rule['fieldId'] );
			$source_value   = empty( $entry ) ? self::get_field_value( $source_field, $field_values ) : self::get_lead_field_value( $entry, $source_field );

			// Number format will either be currency or decimal_dot. Numbers formatted with decimal_comma will have their values transformed and stored as decimal_dot.
			$number_format  = rgobj( $source_field, 'numberFormat' ) == 'currency' ? 'currency' : 'decimal_dot';
			$source_value = GFCommon::maybe_format_numeric( $source_value, $rule['operator'], $number_format );

			/**
			 * Filter the source value of a conditional logic rule before it is compared with the target value.
			 *
			 * @since 2.6.2
			 *
			 * @param int|string $source_value The value of the rule's configured field ID, entry meta, or custom property.
			 * @param array      $rule         The conditional logic rule that is being evaluated.
			 * @param array      $form         The current form meta.
			 * @param array      $logic        All details required to evaluate an objects conditional logic.
			 * @param array      $entry        The current entry object (if available).
			 */
			$source_value = apply_filters( 'gform_rule_source_value', $source_value, $rule, $form, $logic, $entry );

			$is_value_match = self::is_value_match( $source_value, $rule['value'], $rule['operator'], $source_field, $rule, $form );

			if ( $is_value_match ) {
				$match_count ++;
			}
		}

		$do_action = ( $logic['logicType'] == 'all' && $match_count == sizeof( $logic['rules'] ) ) || ( $logic['logicType'] == 'any' && $match_count > 0 );
		$is_hidden = ( $do_action && $logic['actionType'] == 'hide' ) || ( ! $do_action && $logic['actionType'] == 'show' );

		return ! $is_hidden;
	}

	/**
	 * Returns all the draft submissions.
	 *
	 * @since 2.4
	 *
	 * @return array|null|object The query result.
	 */
	public static function get_draft_submissions() {
		global $wpdb;

		self::purge_expired_draft_submissions();

		$table = version_compare( self::get_database_version(), '2.3-dev-1', '<' ) ? self::get_incomplete_submissions_table_name() : self::get_draft_submissions_table_name();
		$sql   = "SELECT uuid, date_created, form_id, ip, submission, source_url FROM {$table}";
		$rows   = $wpdb->get_results( $sql, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

		foreach ( $rows as $row ) {
			$form = self::get_form_meta( $row['form_id'] );
			$row['submission'] = self::filter_draft_submission_post_get( $row['submission'], $row['uuid'], $form );
		}

		return $rows;
	}

	/**
	 * Sanitizes the names of the files that have been uploaded to the tmp directory and sent in
	 * $_POST['gform_uploaded_files'] and caches them in GFFormsModel::$uploaded_files.
	 *
	 * @since 2.4.3.5
	 * @since 2.9.18 Added support for dynamically populated file URLs using the `url` key.
	 * @since 2.10.3 Deprecated the string-based (file/basename) input value.
	 *
	 * @param int $form_id The ID of the form the submission is being processed for.
	 *
	 * @return array
	 */
	public static function set_uploaded_files( $form_id ) {
		$files = GFCommon::json_decode( rgpost( 'gform_uploaded_files' ) );
		if ( ! is_array( $files ) ) {
			$files = array();
		}

		foreach ( $files as $input_name => &$input_files ) {
			if ( empty( $input_files ) ) {
				unset( $files[ $input_name ] );
				continue;
			}

			$field = null;
			if ( preg_match( '/^input_(\d+)$/', $input_name, $matches ) ) {
				$field = GFFormsModel::get_field( $form_id, $matches[1] );
				if ( $field instanceof GF_Field_FileUpload ) {
					$field->formId = (int) $form_id;
				}
			}

			if ( is_array( $input_files ) ) {
				foreach ( $input_files as $key => &$file ) {
					if ( empty( $file ) || ! is_array( $file ) ) {
						unset( $input_files[ $key ] );
						continue;
					}

					// All files regardless of upload or population method should have this.
					if ( isset( $file['uploaded_filename'] ) ) {
						$file['uploaded_filename'] = sanitize_file_name( wp_basename( $file['uploaded_filename'] ) );
					}

					// All multi-file uploads should have this. Single file uploads should have it once a submission or pagination request has been processed.
					if ( isset( $file['temp_filename'] ) ) {
						$file['temp_filename'] = sanitize_file_name( wp_basename( $file['temp_filename'] ) );
					}

					// Sanitize or generate the UUID to be used by the file preview and error messages markup.
					if ( isset( $file['id'] ) ) {
						$file['id'] = sanitize_key( $file['id'] );
					} else {
						$file['id'] = GFFormsModel::get_uuid();
					}

					if ( isset( $file['url'] ) ) {
						$file['url']  = esc_url_raw( $file['url'] );
						$file['hash'] = isset( $file['hash'] ) ? sanitize_text_field( $file['hash'] ) : '';

						if ( ! $field instanceof GF_Field_FileUpload || ! $field->is_valid_populated_file_url( $file ) ) {
							GFCommon::log_debug( __METHOD__ . sprintf( '(): Removing URL %s. File uploads must be submitted as binary uploads.', $input_name ) );
							unset( $input_files[ $key ] );
							continue;
						}
					}
				}
				unset( $file );

				$input_files = array_values( $input_files );
				if ( empty( $input_files ) ) {
					unset( $files[ $input_name ] );
				}
			} else {
				if ( GFCommon::is_valid_url( $input_files ) ) {
					GFCommon::log_debug( __METHOD__ . sprintf( '(): Removing URL %s. File uploads must be submitted as binary uploads.', $input_name ) );
					unset( $files[ $input_name ] );
					continue;
				}

				// Deprecated, retaining for backwards compatibility with third-party integrations.
				$input_files = wp_basename( $input_files );
			}
		}

		self::$uploaded_files[ $form_id ] = $files;

		return $files;
	}

	/**
	 * Checks if an entry exists for the supplied ID.
	 *
	 * @since 2.4.6
	 * @since 2.4.24 Updated to use GFFormsModel::id_exists_in_table().
	 *
	 * @param int $entry_id The ID to be checked.
	 *
	 * @return bool
	 */
	public static function entry_exists( $entry_id ) {
		return self::id_exists_in_table( $entry_id, GFFormsModel::get_entry_table_name() );
	}

	/**
	 * Checks if an id exists in the specified table.
	 *
	 * @since 2.4.24
	 *
	 * @param int    $id    The ID to be checked.
	 * @param string $table The table name, including the site's database prefix.
	 *
	 * @return bool
	 */
	public static function id_exists_in_table( $id, $table ) {
		$id = intval( $id );
		if ( $id <= 0 ) {
			return false;
		}

		global $wpdb;
		$sql    = $wpdb->prepare( "SELECT count(id) FROM %i WHERE id = %d", $table, $id );
		$result = intval( $wpdb->get_var( $sql ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

		return $result > 0;
	}

	/**
	 * Returns an array of column names used by the gf_addon_feed table.
	 *
	 * @since 2.4.24
	 *
	 * @return string[]
	 */
	public static function get_addon_feed_db_columns() {
		return array( 'id', 'form_id', 'is_active', 'feed_order', 'meta', 'addon_slug', 'event_type' );
	}

	/**
	 * Updates the specified feed with the given property value.
	 *
	 * @since 2.4.24
	 *
	 * @since 2.7.17 Added support for encrypting settings fields.
	 *
	 * @param int    $feed_id        The ID of the feed being updated.
	 * @param string $property_name  The name of the property (column) being updated.
	 * @param mixed  $property_value The new value of the specified property.
	 *
	 * @return bool|WP_Error
	 */
	public static function update_feed_property( $feed_id, $property_name, $property_value ) {
		if ( $property_name === 'id' ) {
			return new WP_Error( 'invalid_property', __( 'Updating the id property is not supported', 'gravityforms' ) );
		}

		if ( ! in_array( $property_name, self::get_addon_feed_db_columns() ) ) {
			return new WP_Error( 'invalid_property', sprintf( __( '%s is not a valid feed property', 'gravityforms' ), $property_name ) );
		}

		if ( gf_upgrade()->get_submissions_block() ) {
			return new WP_Error( 'submissions_blocked', __( 'Submissions are currently blocked due to an upgrade in progress', 'gravityforms' ) );
		}

		if ( $property_name === 'meta' ) {
			$is_valid_format = ( is_array( $property_value ) && is_string( key( $property_value ) ) ) || ( is_string( $property_value ) && strpos( $property_value, '{' ) === 0 );
			if ( ! $is_valid_format ) {
				return new WP_Error( 'invalid_meta', __( 'Feed meta should be an associative array or JSON', 'gravityforms' ) );
			}

			$feed = GFAPI::get_feed( $feed_id );
			if ( is_wp_error( $feed ) ) {
				return $feed;
			}
			$meta           = is_array( $property_value ) ? $property_value : json_decode( $property_value, true );
			$encrypted_meta = GFAPI::encrypt_feed_meta( $meta, $feed['addon_slug'] );
			$property_value = json_encode( $encrypted_meta );
		}

		global $wpdb;
		$table  = self::get_addon_feed_table_name();
		$result = $wpdb->update( $table, array( $property_name => $property_value ), array( 'id' => $feed_id ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.DirectQuery

		if ( $result === false ) {
			return new WP_Error( 'error_updating', sprintf( __( 'There was an error while updating feed id %s', 'gravityforms' ), $feed_id ) );
		}

		if ( $result === 0 && ! self::id_exists_in_table( $feed_id, $table ) ) {
			return new WP_Error( 'not_found', sprintf( __( 'Feed id %s not found', 'gravityforms' ), $feed_id ) );
		}

		return (bool) $result;
	}

	/**
	 * Updates the license key, If multisite, it updates the license key for all sites in the network.
	 *
	 * @since 2.7
	 * @since 2.8.17 Updated to also store the key as a network option.
	 *
	 * @param string $license The license key MD5.
	 *
	 * @return void
	 */
	public static function update_license_key( $license ) {
		if ( is_main_site() && GFCommon::is_network_active() ) {
			if ( $license ) {
				update_network_option( null, GFForms::LICENSE_KEY_OPT, $license );
			} else {
				delete_network_option( null, GFForms::LICENSE_KEY_OPT );
			}

			$ids = get_sites( array(
				'fields'       => 'ids',
				'number'       => 0,
				'site__not_in' => array( get_current_blog_id() ),
			) );

			foreach ( $ids as $id ) {
				switch_to_blog( $id );
				if ( $license ) {
					update_option( GFForms::LICENSE_KEY_OPT, $license );
					delete_option( 'gform_pending_installation' );
					delete_option( 'rg_gforms_message' );
				} else {
					delete_option( GFForms::LICENSE_KEY_OPT );
				}
				restore_current_blog();
			}
		}

		if ( $license ) {
			update_option( GFForms::LICENSE_KEY_OPT, $license );
		} else {
			delete_option( GFForms::LICENSE_KEY_OPT );
		}
	}

}

class RGFormsModel extends GFFormsModel {
}

/**
 * In-memory cache of entry meta using "{blog_id}_{entry_id}_{meta_key}" as the key.
 *
 * @since 2.3 Prefixed cache key with the blog id.
 * @since unknown
 *
 * @global array $_gform_lead_meta
 */
global $_gform_lead_meta;
$_gform_lead_meta = array();

// Functions to handle lead meta
function gform_get_meta( $entry_id, $meta_key ) {

	if ( version_compare( GFFormsModel::get_database_version(), '2.3-dev-1', '<' ) ) {
		return GF_Forms_Model_Legacy::gform_get_meta( $entry_id, $meta_key );
	}

	global $wpdb, $_gform_lead_meta;

	//get from cache if available
	$cache_key = get_current_blog_id() . '_' . $entry_id . '_' . $meta_key;
	if ( array_key_exists( $cache_key, $_gform_lead_meta ) ) {
		return maybe_unserialize( $_gform_lead_meta[ $cache_key ] );
	}

	$table_name                   = GFFormsModel::get_entry_meta_table_name();
	$results                      = $wpdb->get_results( $wpdb->prepare( "SELECT meta_value FROM %i WHERE entry_id=%d AND meta_key=%s", $table_name, $entry_id, $meta_key ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.DirectQuery
	$value                        = isset( $results[0] ) ? $results[0]->meta_value : null;
	$meta_value                   = $value === null ? false : maybe_unserialize( $value );
	$_gform_lead_meta[ $cache_key ] = $meta_value;

	return $meta_value;
}

function gform_get_meta_values_for_entries( $entry_ids, $meta_keys ) {
	global $wpdb;

	if ( version_compare( GFFormsModel::get_database_version(), '2.3-dev-1', '<' ) ) {
		return GF_Forms_Model_Legacy::gform_get_meta_values_for_entries( $entry_ids, $meta_keys );
	}

	if ( empty( $meta_keys ) || empty( $entry_ids ) ) {
		return array();
	}

	$table_name            = RGFormsModel::get_entry_meta_table_name();
	$meta_key_select_array = array();

	foreach ( $meta_keys as $meta_key ) {
		$meta_key_select_array[] = "max(case when meta_key = '$meta_key' then meta_value end) as `$meta_key`";
	}

	$entry_ids_str = join( ',', $entry_ids );

	$meta_key_select = join( ',', $meta_key_select_array );

	$sql_query = "  SELECT
                    entry_id, $meta_key_select
                    FROM $table_name
                    WHERE entry_id IN ($entry_ids_str)
                    GROUP BY entry_id";

	$results = $wpdb->get_results( $sql_query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

	foreach ( $results as $result ) {
		foreach ( $meta_keys as $meta_key ) {
			$result->$meta_key = $result->$meta_key === null ? false : maybe_unserialize( $result->$meta_key );
		}
	}

	$meta_value_array = $results;

	return $meta_value_array;
}

/**
 * Add or update metadata associated with an entry.
 *
 * Data will be serialized. Don't forget to sanitize user input.
 *
 * @since Unknown
 * @since 2.5 Return the result of the query.
 *
 * @param int      $entry_id   The ID of the entry to be updated.
 * @param string   $meta_key   The key for the meta data to be stored.
 * @param mixed    $meta_value The data to be stored for the entry.
 * @param int|null $form_id    The form ID of the entry (optional, saves extra query if passed when creating the metadata).
 *
 * @return int|false           Returns the number of affected rows, or false if the operation fails.
 */
function gform_update_meta( $entry_id, $meta_key, $meta_value, $form_id = null ) {
	global $wpdb, $_gform_lead_meta;

	if ( gf_upgrade()->get_submissions_block() ) {
		return;
	}

	if ( intval( $entry_id ) <= 0 ) {
		return;
	}

	if ( version_compare( GFFormsModel::get_database_version(), '2.3-dev-1', '<' ) ) {
		GF_Forms_Model_Legacy::gform_update_meta( $entry_id, $meta_key, $meta_value, $form_id );
		return;
	}
	$table_name = GFFormsModel::get_entry_meta_table_name();


	if ( false === $meta_value ) {
		$meta_value = '0';
	}

	$serialized_meta_value  = maybe_serialize( $meta_value );
	$meta_exists = gform_get_meta( $entry_id, $meta_key ) !== false;
	if ( $meta_exists ) {
		$result = $wpdb->update( $table_name, array( 'meta_value' => $serialized_meta_value ), array( 'entry_id' => $entry_id, 'meta_key' => $meta_key ), array( '%s' ), array( '%d', '%s' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.SlowDBQuery.slow_db_query_meta_value
	} else {

		if ( empty( $form_id ) ) {
			$entry_table_name = GFFormsModel::get_entry_table_name();
			$form_id         = $wpdb->get_var( $wpdb->prepare( "SELECT form_id from %i WHERE id=%d", $entry_table_name, $entry_id ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		} else {
			$form_id = intval( $form_id );
		}

		$result = $wpdb->insert( $table_name, array( 'form_id' => $form_id, 'entry_id' => $entry_id, 'meta_key' => $meta_key, 'meta_value' => $serialized_meta_value ), array( '%d', '%d', '%s', '%s' ) ); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value, WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.DirectDatabaseQuery.DirectQuery
	}

	if ( $result !== false ) {
		//updates cache
		$cache_key = get_current_blog_id() . '_' . $entry_id . '_' . $meta_key;
		if ( array_key_exists( $cache_key, $_gform_lead_meta ) ) {
			$_gform_lead_meta[ $cache_key ] = $meta_value;
		}
	}

	return $result;
}

/**
 * Add metadata associated with an entry.
 *
 * Data will be serialized; Don't forget to sanitize user input.
 *
 * @since Unknown
 * @since 2.5 Return the result of the query.
 *
 * @param int      $entry_id   The ID of the entry where metadata is to be added.
 * @param string   $meta_key   The key for the meta data to be stored.
 * @param mixed    $meta_value The data to be stored for the entry.
 * @param int|null $form_id    The form ID of the entry (optional, saves extra query if passed when creating the metadata).
 *
 * @return int|false           Returns the number of affected rows, or false if the operation fails.
 */
function gform_add_meta( $entry_id, $meta_key, $meta_value, $form_id = null ) {
	global $wpdb, $_gform_lead_meta;

	if ( gf_upgrade()->get_submissions_block() ) {
		return;
	}

	if ( intval( $entry_id ) <= 0 ) {
		return;
	}

	if ( version_compare( GFFormsModel::get_database_version(), '2.3-dev-1', '<' ) ) {
		GF_Forms_Model_Legacy::gform_add_meta( $entry_id, $meta_key, $meta_value, $form_id );
		return;
	}

	$table_name = RGFormsModel::get_entry_meta_table_name();

	if ( false === $meta_value ) {
		$meta_value = '0';
	}
	$serialized_meta_value  = maybe_serialize( $meta_value );

	if ( empty( $form_id ) ) {
		$entry_table_name = GFFormsModel::get_entry_table_name();
		$form_id         = $wpdb->get_var( $wpdb->prepare( "SELECT form_id from %i WHERE id=%d", $entry_table_name, $entry_id ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
	} else {
		$form_id = intval( $form_id );
	}

	$result = $wpdb->insert( $table_name, array( 'form_id' => $form_id, 'entry_id' => $entry_id, 'meta_key' => $meta_key, 'meta_value' => $serialized_meta_value ), array( '%d', '%d', '%s', '%s' ) ); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value, WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.DirectDatabaseQuery.DirectQuery

	if ( $result !== false ) {
		$cache_key                      = get_current_blog_id() . '_' . $entry_id . '_' . $meta_key;
		$_gform_lead_meta[ $cache_key ] = $meta_value;
	}

	return $result;
}

/**
 * Delete metadata associated with an entry.
 *
 * @since Unknown
 * @since 2.5 Return the result of the query.
 *
 * @param int    $entry_id The ID of the entry to be deleted.
 * @param string $meta_key The key for the meta data to be deleted.
 *
 * @return int|bool       Returns true or the number of rows affected, or false if the operation fails.
 */
function gform_delete_meta( $entry_id, $meta_key = '' ) {
	global $wpdb, $_gform_lead_meta;

	if ( gf_upgrade()->get_submissions_block() ) {
		return;
	}

	if ( version_compare( GFFormsModel::get_database_version(), '2.3-dev-1', '<' ) ) {
		GF_Forms_Model_Legacy::gform_delete_meta( $entry_id, $meta_key );
		return;
	}

	$table_name  = RGFormsModel::get_entry_meta_table_name();
	$meta_filter = empty( $meta_key ) ? '' : $wpdb->prepare( 'AND meta_key=%s', $meta_key );

	$result = $wpdb->query( $wpdb->prepare( "DELETE FROM %i WHERE entry_id=%d {$meta_filter}", $table_name, $entry_id ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

	if ( $result !== false ) {
		//clears cache.
		$_gform_lead_meta = array();
	}

	return $result;
}
