<?php

	// Exit if accessed directly
	if ( ! defined( 'ABSPATH' ) ) {
		exit;
	}

	#[AllowDynamicProperties]
	class WS_Form_Submit extends WS_Form_Core {

		public $id = 0;
		public $form_id;
		public $date_added;
		public $date_updated;
		public $date_expire;
		public $user_id;
		public $hash;
		public $token;
		public $token_validated;
		public $duration;
		public $count_submit;
		public $status;
		public $actions;
		public $section_repeatable;
		public $preview;
		public $spam_level;
		public $starred;
		public $viewed;

		public $meta;
		public $meta_protected;

		public $post_mode;

		public $row_id_filter;

		public $form_object;

		public $error;
		public $error_message;
		public $error_code;

		public $error_validation_actions;

		public $encrypted;

		public $table_name;
		public $table_name_meta;

		public $bypass_required_array;
		public $hidden_array;

		public $field_types;

		public $field_object_cache = array();

		public $file_objects = array();

		public $submit_fields = false;

		public $return_hash = false;

		public $keys = false;
		public $keys_meta = false;
		public $keys_fixed = false;
		public $keys_fields = false;
		public $keys_ecommerce = false;
		public $keys_tracking = false;

		public $form_count_submit_cache = false;
		public $form_count_submit_unread_cache = false;

		const DB_INSERT = 'form_id,date_added,date_updated,date_expire,user_id,hash,token,token_validated,duration,count_submit,status,actions,section_repeatable,preview,spam_level,starred,viewed,encrypted';
		const DB_UPDATE = 'form_id,date_added,date_updated,date_expire,user_id,hash,token,token_validated,duration,count_submit,status,actions,section_repeatable,preview,spam_level,starred,viewed,encrypted';
		const DB_SELECT = 'form_id,date_added,date_updated,date_expire,user_id,hash,token,token_validated,duration,count_submit,status,actions,section_repeatable,preview,spam_level,starred,viewed,encrypted,id';

		public function __construct() {

			global $wpdb;

			$this->id = 0;
			$this->form_id = 0;
			$this->user_id = get_current_user_id();
			$this->hash = '';
			$this->token = false;
			$this->token_validated = false;
			$this->status = 'draft';
			$this->duration = 0;
			$this->count_submit = 0;
			$this->meta = array();
			$this->meta_protected = array();
			$this->actions = '';
			$this->section_repeatable = '';
			$this->preview = false;
			$this->date_added = WS_Form_Common::get_mysql_date();
			$this->date_updated = WS_Form_Common::get_mysql_date();
			$this->date_expire = null;
			$this->spam_level = null;
			$this->starred = false;
			$this->viewed = false;

			$this->post_mode = false;
			$this->row_id_filter = false;

			$this->error = false;
			$this->error_message = '';
			$this->error_code = 200;

			$this->error_validation_actions = array();

			$this->encrypted = false;
			// Get field types in single dimension array
			$this->field_types = false;

			$this->table_name = sprintf('%s%ssubmit', $wpdb->prefix, WS_FORM_DB_TABLE_PREFIX);
			$this->table_name_meta = sprintf('%s_meta', $this->table_name);
		}

		// Create
		public function db_create($update_count_submit_unread = true) {

			// No capabilities required, this is a public method

			// Check form ID
			self::db_check_form_id();

			global $wpdb;

			// Insert submit record

			// Handle NULL values because $wpdb->prepare does not
			$sql_prepare = sprintf(

				"INSERT INTO {$wpdb->prefix}wsf_submit (form_id,date_added,date_updated,date_expire,user_id,hash,token,token_validated,duration,count_submit,status,actions,section_repeatable,preview,spam_level,starred,viewed,encrypted) VALUES (%%d, %%s, %%s, %s, %%d, '', '', 0, %%d, %%d, %%s, %%s, %%s, %%d, %s, %%d, %%d, %%d);",
				(is_null($this->date_expire) ? 'NULL' : "'" . $this->date_expire . "'"),
				(is_null($this->spam_level) ? 'NULL' : $this->spam_level)
			);

			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Custom database table, already escaped
			if($wpdb->query($wpdb->prepare(

				$sql_prepare, // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
				$this->form_id,
				$this->date_added,
				$this->date_updated,
				$this->user_id,
				$this->duration,
				$this->count_submit,
				$this->status,
				$this->actions,
				$this->section_repeatable,
				($this->preview ? 1 : 0),
				($this->starred ? 1 : 0),
				($this->viewed ? 1 : 0),
				($this->encrypted ? 1 : 0)
			)) === false) { parent::db_wpdb_handle_error(__('Error adding submit', 'ws-form')); }

			// Get inserted ID
			$this->id = $wpdb->insert_id;

			// Create hash
			self::db_create_hash();

			// Create token
			self::db_create_token();

			// Update hash
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom database table
			$update_result = $wpdb->update(
				"{$wpdb->prefix}wsf_submit",
				array(
					'hash' => $this->hash,
					'token' => $this->token,
				),
				array( 'id' => $this->id ),
				array( '%s', '%s' ),
				array( '%d' )
			);

			if($update_result === false) { 
				parent::db_wpdb_handle_error(__('Error updating submit.', 'ws-form')); 
			}

			// Update form submit unread count statistic
			if($update_count_submit_unread) {

				$ws_form_form = new WS_Form_Form();
				$ws_form_form->id = $this->form_id;
				$ws_form_form->db_update_count_submit_unread(true);
			}

			// Run action
			// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- All hooks prefixed with wsf_
			do_action('wsf_submit_create', $this);
		}

		// Read record to array
		public function db_read($get_meta = true, $get_expanded = true, $bypass_user_capability_check = false) {

			// User capability check
			WS_Form_Common::user_must('read_submission', $bypass_user_capability_check);

			self::db_check_id();

			global $wpdb;

			// Add fields
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom database table
			$submit_array = $wpdb->get_row($wpdb->prepare(

				"SELECT form_id,date_added,date_updated,date_expire,user_id,hash,token,token_validated,duration,count_submit,status,actions,section_repeatable,preview,spam_level,starred,viewed,encrypted,id FROM {$wpdb->prefix}wsf_submit WHERE id = %d LIMIT 1;",
				$this->id
			), 'ARRAY_A');
			if(is_null($submit_array)) { parent::db_wpdb_handle_error(__('Unable to read submission.', 'ws-form')); }

			// Set class variables
			foreach($submit_array as $key => $value) {

				$this->{$key} = $value;
			}

			// Convert into object
			$submit_object = json_decode(wp_json_encode($submit_array));

			// Process meta data
			if($get_meta) {

				$this->meta = $submit_object->meta = self::db_get_submit_meta($submit_object, false, $bypass_user_capability_check);
			}

			// Get user data
			if($get_expanded) {

				self::db_read_expanded($submit_object, true, true, true, true, true, true, true, $bypass_user_capability_check);
			}

			// Preview to boolean
			if(isset($this->preview)) { $this->preview = $submit_object->preview = (bool) $this->preview; }

			// Encrypted to boolean
			if(isset($this->encrypted)) { $this->encrypted = $submit_object->encrypted = (bool) $this->encrypted; }

			// Return array
			return $submit_object;
		}

		// Read expanded data for a record
		public function db_read_expanded(&$submit_object, $expand_user = true, $expand_date_added = true, $expand_date_updated = true, $expand_status = true, $expand_actions = true, $expand_section_repeatable = true, $expand_file_objects = true, $bypass_user_capability_check = false) {

			// User capability check
			WS_Form_Common::user_must('read_submission', $bypass_user_capability_check);

			if(
				!$bypass_user_capability_check &&	// Do not expand user data if this is a public request
				$expand_user &&
				isset($submit_object->user_id) &&
				($submit_object->user_id > 0)
			) {

				$user = get_user_by('ID', $submit_object->user_id);
				if($user !== false) {

					$this->user = $submit_object->user = (object) array(

						'first_name' 	=>	$user->first_name,
						'last_name' 	=>	$user->last_name,
						'display_name'	=> $user->display_name
					);
				}
			}

			// Date added
			if($expand_date_added && isset($submit_object->date_added)) {

				$this->date_added_wp = $submit_object->date_added_wp = date_i18n(get_option('date_format') . ' ' . get_option('time_format'), strtotime(get_date_from_gmt($submit_object->date_added)));
			}

			// Date updated
			if($expand_date_updated && isset($submit_object->date_updated)) {

				$this->date_updated_wp = $submit_object->date_updated_wp = date_i18n(get_option('date_format') . ' ' . get_option('time_format'), strtotime(get_date_from_gmt($submit_object->date_updated)));
			}

			// Status
			if($expand_status && isset($submit_object->status)) {

				$this->status_full = $submit_object->status_full = self::db_get_status_name($submit_object->status);
			}

			// Unserialize actions
			if($expand_actions && isset($submit_object->actions)) {

				$this->actions = $submit_object->actions = is_serialized($submit_object->actions) ? WS_Form_Common::maybe_unserialize($submit_object->actions) : false;
			}

			// Unserialize section_repeatable
			if($expand_section_repeatable && isset($submit_object->section_repeatable)) {

				$this->section_repeatable = $submit_object->section_repeatable = is_serialized($submit_object->section_repeatable) ? WS_Form_Common::maybe_unserialize($submit_object->section_repeatable) : false;
			}

			// File objects
			if($expand_file_objects && isset($submit_object->meta)) {

				$metas = (array) $submit_object->meta;

				foreach($metas as $meta_key => $meta) {

					$meta = (array) $meta;

					// Add URLs to file objects all objects
					if(
						isset($meta['type']) &&
						(($meta['type'] == 'file') || ($meta['type'] == 'mediacapture') || ($meta['type'] == 'signature')) &&
						isset($meta['value']) &&
						is_array($meta['value']) &&
						(count($meta['value']) > 0) &&
						is_array($meta['value'][0]) &&
						isset($meta['id'])
					) {

						foreach($meta['value'] as $file_object_index => $file_object) {

							if(
								isset($file_object['url']) ||
								!isset($file_object['name']) ||
								!isset($file_object['size']) ||
								!isset($file_object['type']) ||
								!isset($file_object['path'])

							) { continue; }

							// Get handler
							$handler = isset($file_object['handler']) ? $file_object['handler'] : 'wsform';

							// Get URL
							if(isset(WS_Form_File_Handler::$file_handlers[$handler])) {

								$section_repeatable_index = isset($meta['repeatable_index']) ? absint($meta['repeatable_index']) : 0;

								$url = WS_Form_File_Handler::$file_handlers[$handler]->get_url($file_object, $meta['id'], $file_object_index, $submit_object->hash, $section_repeatable_index);

							} else {

								$url = '#';
							}

							// Set URL
							$this->meta[$meta_key]['value'][$file_object_index]['url'] = $submit_object->meta[$meta_key]['value'][$file_object_index]['url'] = $url;

							// Set preview if attachment ID is set
							if(isset($file_object['attachment_id'])) {

								// Get image size
								// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- All hooks prefixed with wsf_
								$image_size = apply_filters('wsf_dropzonejs_image_size', WS_FORM_DROPZONEJS_IMAGE_SIZE);

								$attachment_id = $file_object['attachment_id'];

								$file_preview = wp_get_attachment_image_src($attachment_id, $image_size, true);
								if($file_preview) {

									$file_preview = $file_preview[0];

								} else {

									$file_preview = wp_get_attachment_thumb_url($attachment_id);

									if(!$file_preview) { $file_preview = ''; }
								}
								if(!$file_preview) { $file_preview = ''; }

								$this->meta[$meta_key]['value'][$file_object_index]['preview'] = $submit_object->meta[$meta_key]['value'][$file_object_index]['preview'] = $file_preview;
							}
						}
					}
				}
			}
		}

		// Read - All
		public function db_read_all($join = '', $where = '', $group_by = '', $order_by = '', $limit = '', $offset = '', $get_meta = true, $get_expanded = true, $bypass_user_capability_check = false, $clear_hidden_fields = false) {

			global $wpdb;

			// User capability check
			WS_Form_Common::user_must('read_submission', $bypass_user_capability_check);

			// Build SQL
			$sql = sprintf(

				'SELECT %s FROM %s',
				self::get_select($join),
				$this->table_name
			);

			if($join != '') { $sql .= sprintf(" %s", $join); }
			if($where != '') { $sql .= sprintf(" WHERE %s", $where); }
			if($group_by != '') { $sql .= sprintf(" GROUP BY %s", $group_by); }
			if($order_by != '') { $sql .= sprintf(" ORDER BY %s", $order_by); }
			if($limit != '') { $sql .= sprintf(" LIMIT %u", absint($limit)); }
			if($offset != '') { $sql .= sprintf(" OFFSET %u", absint($offset)); }

			$sql .= ';';

			// Get results
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Custom database table, already escaped
			$return_array = $wpdb->get_results($sql);

			if(is_null($return_array)) { return; }

			foreach($return_array as $key => $submit_object) {

				// Check form ID
				if(absint($submit_object->form_id) === 0) {

					// Delete this orphaned submit record
					$this->id = $submit_object->id;
					self::db_delete(true);

					// Remove from return array
					unset($return_array[$key]);

					continue;
				}

				// Process meta data
				if($get_meta) {

					// Get meta data
					$submit_object->meta = self::db_get_submit_meta($submit_object, false, $bypass_user_capability_check);

					// Clear hidden fields
					if($clear_hidden_fields) {

						$submit_object = self::clear_hidden_meta_values($submit_object);
					}
				}
	
				// Process expanded data
				if($get_expanded) {

					self::db_read_expanded($submit_object, true, true, true, true, true, true, true, $bypass_user_capability_check);
				}

				$return_array[$key] = $submit_object;
			}

			return $return_array;
		}

		// Read - All
		public function db_read_ids($join = '', $where = '', $group_by = '', $order_by = '', $limit = '', $offset = '', $bypass_user_capability_check = false) {

			// User capability check
			WS_Form_Common::user_must('read_submission', $bypass_user_capability_check);

			// Build SQL
			$sql = sprintf(

				'SELECT %1$s.id FROM %1$s',
				$this->table_name
			);

			if($join != '') { $sql .= sprintf(" %s", $join); }
			if($where != '') { $sql .= sprintf(" WHERE %s", $where); }
			if($group_by != '') { $sql .= sprintf(" GROUP BY %s", $group_by); }
			if($order_by != '') { $sql .= sprintf(" ORDER BY %s", $order_by); }
			if($limit != '') { $sql .= sprintf(" LIMIT %u", absint($limit)); }
			if($offset != '') { $sql .= sprintf(" OFFSET %u", absint($offset)); }

			$sql .= ';';

			global $wpdb;

			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Custom database table, already escaped
			$return_array = $wpdb->get_results($sql, 'ARRAY_A');

			return empty($return_array) ? null : array_column($return_array, 'id');
		}

		// Read - Count
		public function db_read_count($join = '', $where = '', $bypass_user_capability_check = false) {

			// User capability check
			WS_Form_Common::user_must('read_submission', $bypass_user_capability_check);

			global $wpdb;

			// Build SQL
			$sql = sprintf(

				'SELECT COUNT(%1$s.id) FROM %1$s',
				$this->table_name
			);

			if($join != '') { $sql .= sprintf(" %s", $join); }
			if($where != '') { $sql .= sprintf(" WHERE %s", $where); }

			$sql .= ';';

			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Custom database table, already escaped
			$read_count = $wpdb->get_var($sql);
			if(is_null($read_count)) { return 0; }

			return absint($read_count);
		}

		// Read by hash
		public function db_read_by_hash($get_meta = true, $get_expanded = true, $form_id_check = true, $bypass_user_capability_check = false) {

			// User capability check
			WS_Form_Common::user_must('read_submission', $bypass_user_capability_check);

			// Check form ID
			if($form_id_check) { self::db_check_form_id(); }

			// Check hash
			if(!WS_Form_Common::check_submit_hash($this->hash)) {

				$this->hash = '';

				self::return_forbidden();
			}

			// Check token
			if($this->token !== false) {

				if(!WS_Form_Common::check_submit_hash($this->token)) {

					$this->token = '';

					self::return_forbidden();
				}

				$token_check = $this->token;

			} else {

				$token_check = false;
			}

			global $wpdb;

			// Get form submission
			if($form_id_check) {

				// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom database table
				$submit_array = $wpdb->get_row($wpdb->prepare(

					"SELECT form_id,date_added,date_updated,date_expire,user_id,hash,token,token_validated,duration,count_submit,status,actions,section_repeatable,preview,spam_level,starred,viewed,encrypted,id FROM {$wpdb->prefix}wsf_submit WHERE form_id = %d AND hash = %s AND (NOT status = 'trash') LIMIT 1;",
					$this->form_id,
					$this->hash
				), 'ARRAY_A');

			} else {

				// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom database table
				$submit_array = $wpdb->get_row($wpdb->prepare(

					"SELECT form_id,date_added,date_updated,date_expire,user_id,hash,token,token_validated,duration,count_submit,status,actions,section_repeatable,preview,spam_level,starred,viewed,encrypted,id FROM {$wpdb->prefix}wsf_submit WHERE hash = %s AND (NOT status = 'trash') LIMIT 1;",
					$this->hash
				), 'ARRAY_A');				
			}

			if(is_null($submit_array)) {

				$this->hash = '';

				parent::db_wpdb_handle_error(__("Submission record not found. Ensure you have a 'Save Submission' action.", 'ws-form'));
			}

			// Set class variables
			foreach($submit_array as $key => $value) {

				$this->{$key} = $value;
			}

			// Convert into object
			$submit_object = json_decode(wp_json_encode($submit_array));

			// Process meta data
			if($get_meta) {

				$this->meta = $submit_object->meta = self::db_get_submit_meta($submit_object, false, $bypass_user_capability_check);
			}

			// Expand data
			if($get_expanded) {

				self::db_read_expanded($submit_object, true, true, true, true, true, true, true, $bypass_user_capability_check);
			}

			// Perform token validation
			if(!$this->token_validated && ($token_check !== false)) {

				if($this->token === $token_check) {

					$this->token_validated = $submit_object->token_validated = true;

					// Update hash
					// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom database table
			$update_result = $wpdb->update(
						"{$wpdb->prefix}wsf_submit",
						array(
							'token_validated' => 1,
							'spam_level' => 0,
						),
						array( 'id' => $this->id ),
						array( '%d', '%d' ),
						array( '%d' )
					);

					if($update_result === false) { 
						parent::db_wpdb_handle_error(__('Error updating submit.', 'ws-form')); 
					}
				}
			}

			// Return array
			return $submit_object;
		}

		// Update current submit
		public function db_update() {

			// No capabilities required, this is a public method

			// Check ID
			self::db_check_id();

			// Update / Insert
			$this->id = parent::db_update_insert($this->table_name, self::DB_UPDATE, self::DB_INSERT, $this, 'submit', $this->id);

			// Update meta
			if(isset($this->meta)) {

				$ws_form_submit_meta = new WS_Form_Submit_Meta();
				$ws_form_submit_meta->parent_id = $this->id;
				$ws_form_submit_meta->db_update_from_object($this->meta, $this->encrypted);
			}

			// Run action
			// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- All hooks prefixed with wsf_
			do_action('wsf_submit_update', $this);
		}

		// Push submit from array
		public function db_update_from_object($submit_object) {

			// No capabilities required, this is a public method

			// Check for submit ID in $submit
			if(isset($submit_object->id)) { $this->id = absint($submit_object->id); } else { return false; }

			// Encryption
			$submit_encrypted = isset($submit_object->encrypted) ? $submit_object->encrypted : false;

			// Update / Insert
			$this->id = parent::db_update_insert($this->table_name, self::DB_UPDATE, self::DB_INSERT, $submit_object, 'submit', $this->id);

			// Update meta
			if(isset($submit_object->meta)) {

				$ws_form_submit_meta = new WS_Form_Submit_Meta();
				$ws_form_submit_meta->parent_id = $this->id;
				$ws_form_submit_meta->db_update_from_object($submit_object->meta, $submit_encrypted);
			}
		}

		// Get post data. Returns the submission object in a key value format 
		public function get_post_data() {

			// Build return array
			$return_array = array(

				'id' => $this->id,
				'form_id' => $this->form_id,
				'date_added' => $this->date_added,
				'date_updated' => $this->date_updated,
				'date_expire' => $this->date_expire,
				'user_id' => $this->user_id,
				'hash' => $this->hash
			);

			// Process meta
			foreach($this->meta as $key => $value) {

				if(strpos($key, WS_FORM_FIELD_PREFIX) === 0) {

					if(!isset($value['value'])) { continue; }

					if(is_array($value['value'])) { $value['value'] = implode(',', $value['value']); }

					$return_array[$key] = $value['value'];
				}
			}

			return $return_array;
		}

		// Stamp submit with date updated, increase submit count and add duration (if available)
		public function db_stamp() {

			// No capabilities required, this is a public method

			// Check ID
			self::db_check_id();

			// Get duration
			$this->duration = absint(WS_Form_Common::get_query_var_nonce('wsf_duration', 0));

			global $wpdb;

			// Date updated, count submit + 1
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom database table
			if($wpdb->query($wpdb->prepare(

				"UPDATE {$wpdb->prefix}wsf_submit SET date_updated = %s, count_submit = count_submit + 1, duration = %d WHERE id = %d LIMIT 1", // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQueryUse -- Needed for count_submit + 1
				WS_Form_Common::get_mysql_date(),
				$this->duration,
				$this->id
			)) === false) { parent::db_wpdb_handle_error(__('Error updating submit date updated.', 'ws-form')); }

			$this->count_submit++;

			// User ID
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom database table
			if($wpdb->query($wpdb->prepare(

				"UPDATE {$wpdb->prefix}wsf_submit SET user_id = %d WHERE id = %d AND (user_id = 0 OR user_id IS NULL) LIMIT 1",
				get_current_user_id(), // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQueryUse -- Needed for OR
				$this->id
			)) === false) { parent::db_wpdb_handle_error(__('Error updating submit user ID,', 'ws-form')); }
		}

		// Delete
		public function db_delete($permanent_delete = false, $count_update = true, $bypass_user_capability_check = false) {

			// User capability check
			WS_Form_Common::user_must('delete_submission', $bypass_user_capability_check);

			self::db_check_id();

			// Read the submit status
			self::db_read(true, false, $bypass_user_capability_check);

			if(in_array($this->status, array('spam', 'trash'))) { $permanent_delete = true; }

			// If status is trashed, do a permanent delete of the data
			if($permanent_delete) {

				global $wpdb;

				// Delete submit
				// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom database table
			$delete_result = $wpdb->delete(
					"{$wpdb->prefix}wsf_submit",
					array( 'id' => $this->id ),
					array( '%d' )
				);

				if($delete_result === false) { 
					parent::db_wpdb_handle_error(__('Error deleting submit.', 'ws-form')); 
				}

				// Delete meta
				$ws_form_meta = new WS_Form_Submit_Meta();
				$ws_form_meta->parent_id = $this->id;
				$ws_form_meta->db_delete_by_submit($bypass_user_capability_check);

				// Run action
				// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- All hooks prefixed with wsf_
				do_action('wsf_submit_delete_permanent', $this);

			} else {

				// Set status to 'trash'
				self::db_set_status('trash', $count_update, $bypass_user_capability_check);
			}

			return true;
		}

		// Delete trashed submits
		public function db_trash_delete() {

			// User capability check
			WS_Form_Common::user_must('delete_submission');

			self::db_check_form_id();

			// Get all trashed forms
			$submits = self::db_read_all('', "status='trash' AND form_id=" . $this->form_id, '', '', '', '', false, false);

			foreach($submits as $submit_object) {

				$this->id = $submit_object->id;
				self::db_delete();
			}

			return true;
		}

		// Export by email
		public function db_exporter($email_address) {

			// User capability check
			WS_Form_Common::user_must('read_submission');

			// Check email address
			if(!filter_var($email_address, FILTER_VALIDATE_EMAIL)) { return false; }

			$data_to_export = array();

			global $wpdb;

			// Get submit records
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom database table
			$submissions = $wpdb->get_results($wpdb->prepare(

				"SELECT {$wpdb->prefix}wsf_submit.id FROM {$wpdb->prefix}wsf_submit_meta LEFT OUTER JOIN {$wpdb->prefix}wsf_submit ON {$wpdb->prefix}wsf_submit.id = {$wpdb->prefix}wsf_submit_meta.parent_id WHERE (LOWER({$wpdb->prefix}wsf_submit_meta.meta_value) = %s) AND NOT ({$wpdb->prefix}wsf_submit.id IS NULL);",
				strtolower($email_address)
			));

			// Process results
			if($submissions) {

				foreach($submissions as $submission) {

					// Reset submit data
					$submit_data = array();

					// Get submit ID
					$submit_id = $submission->id;

					// Get submit record
					$this->id = $submit_id;
					$submit_object = self::db_read();

					// Remove some data that will not be shared for security reasons or internal only
					unset($submit_object->form_id);
					unset($submit_object->user_id);
					unset($submit_object->id);
					unset($submit_object->actions);
					unset($submit_object->preview);
					unset($submit_object->status);

					// Push all submit data
					foreach($submit_object as $key => $value) {

						// Convert objects to array (e.g. user data)
						if(is_object($value)) {

							$value = (array) $value;
						}

						if(is_array($value)) {
							
							foreach($value as $meta_key => $meta_value) {

								if(is_object($meta_value)) {

									$meta_value = (array) $meta_value;
								}

								if(is_array($meta_value)) {

									$value = $meta_value['value'];

									if(is_object($value)) {

										$value = (array) $value;
									}

									if(is_array($value)) {

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

								} else {

									$value = $meta_value;											
								}

								$submit_data[] = array('name' => $meta_key, 'value' => $value);
							}

						} else {
							
							$submit_data[] = array('name' => $key, 'value' => $value);
						}
					}

					$data_to_export[] = array(
						'group_id'    => WS_FORM_USER_REQUEST_IDENTIFIER,
						'group_label' => __('Form Submissions', 'ws-form'),
						'item_id'     => WS_FORM_USER_REQUEST_IDENTIFIER . '-' . $submit_object->hash,
						'data'        => $submit_data
					);
				}
			}

			// Return
			return array(

				'data' => $data_to_export,
				'done' => true,
			);
		}

		// Erase by email
		public function db_eraser($email_address) {

			global $wpdb;

			// User capability check
			WS_Form_Common::user_must('delete_submission');

			// Check email address
			if(!filter_var($email_address, FILTER_VALIDATE_EMAIL)) { return false; }

			// Return array
			$items_removed_count = 0;
			$items_retained_count = 0;

			// Get submit records to be deleted
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom database table
			$submissions = $wpdb->get_results($wpdb->prepare(

				"SELECT {$wpdb->prefix}wsf_submit.id FROM {$wpdb->prefix}wsf_submit_meta LEFT OUTER JOIN {$wpdb->prefix}wsf_submit ON {$wpdb->prefix}wsf_submit.id = {$wpdb->prefix}wsf_submit_meta.parent_id WHERE (LOWER({$wpdb->prefix}wsf_submit_meta.meta_value) = %s) AND NOT ({$wpdb->prefix}wsf_submit.id IS NULL);",
				strtolower($email_address)
			));

			// Process results
			if($submissions) {

				$items_retained_count = count($submissions);

				if($items_retained_count > 0) {

					// Get first record (Delete one record each time eraser is requested to avoid timeouts)
					if(isset($submissions[0]->id)) {

						// Delete submit record with permanent delete
						$this->id = $submissions[0]->id;
						self::db_delete(true);

						$items_retained_count--;
						$items_removed_count++;
					}
				}
			}

			// Build return values
			$items_removed = ($items_removed_count > 0);
			$items_retained = ($items_retained_count > 0);
			$done = ($items_retained_count <= 0);
			$messages = (($items_removed_count > 0) && ($items_retained_count <= 0)) ? array(sprintf(

				/* translators: %s: WS Form */
				__('%s submissions successfully deleted.', 'ws-form'),

				WS_FORM_NAME_GENERIC
			
			)) : array();

			// Return
			return array(

				'items_removed' => $items_removed_count,
				'items_retained' => $items_retained_count,
				'messages' => $messages,
				'done' => $done,
			);
		}

		// Delete expired
		public function db_delete_expired($count_update_all = true, $bypass_user_capability_check = false) {

			global $wpdb;

			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom database table
			$rows_affected = $wpdb->query($wpdb->prepare(

				"UPDATE {$wpdb->prefix}wsf_submit SET status = 'trash', date_expire = NULL WHERE (NOT date_expire IS NULL) AND (NOT date_expire = '0000-00-00 00:00:00') AND (NOT status = 'trash') AND (date_expire < %s)", // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQueryUse -- Needed for complex WHERE
				WS_Form_Common::get_mysql_date()
			));

			// Update form submit unread count statistic
			if($count_update_all) {

				$ws_form_form = new WS_Form_Form();
				$ws_form_form->db_count_update_all($bypass_user_capability_check);
			}

			return $rows_affected;
		}

		// Get submission count by status
		public function db_get_count_by_status($form_id = 0, $status = '') {

			// User capability check
			WS_Form_Common::user_must('read_submission');

			if($form_id == 0) { return 0; }

			global $wpdb;

			// Check status
			$status = WS_Form_Common::check_submit_status($status);

			if($status == '') {

				// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom database table
				$form_count = $wpdb->get_var($wpdb->prepare(

					"SELECT COUNT(id) FROM {$wpdb->prefix}wsf_submit WHERE NOT(status = 'trash' OR status = 'spam') AND form_id = %d;",
					$form_id
				));

			} else {

				// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom database table
				$form_count = $wpdb->get_var($wpdb->prepare(

					"SELECT COUNT(id) FROM {$wpdb->prefix}wsf_submit WHERE status = %s AND form_id = %d",
					$status,
					$form_id
				));
			}

			if(is_null($form_count)) { $form_count = 0; }

			return $form_count; 
		}

		// Remove objects from meta after maybe_unserialize() (allowed_classes includes stdClass). Legitimate values are scalars or nested arrays only.
		private static function submit_meta_value_strip_objects($value, $depth = 0) {

			if($depth > 100) {

				return '';
			}

			if(is_object($value)) {

				return '';
			}

			if(is_array($value)) {

				$result = array();

				foreach($value as $key => $item) {

					$result[$key] = self::submit_meta_value_strip_objects($item, $depth + 1);
				}

				return $result;
			}

			return $value;
		}

		// Get submit meta
		public function db_get_submit_meta($submit_object, $meta_array = false, $bypass_user_capability_check = false) {

			// No capabilities required, this is a public method
			$submit_meta = array();

			// Get submit record ID
			$submit_id = $submit_object->id;
			$submit_encrypted = isset($submit_object->encrypted) ? $submit_object->encrypted : false;

			// Read meta
			if(!is_array($meta_array)) {

				$ws_form_submit_meta = new WS_Form_Submit_Meta();
				$ws_form_submit_meta->parent_id = $submit_id;
				$meta_array = $ws_form_submit_meta->db_read_all($bypass_user_capability_check, $submit_encrypted);
			}

			// Process meta data
			foreach($meta_array as $index => $meta) {

				// Get field value
				$value = WS_Form_Common::maybe_unserialize($meta['meta_value']);

				// Normalize after unserialize (all meta rows, including non-field keys).
				$value = self::submit_meta_value_strip_objects($value);

				// Get field ID
				$field_id = absint($meta['field_id']);

				// If field ID found, process and return as array including type
				if($field_id > 0) {

					// Load field data to cache
					if(isset($this->field_object_cache[$field_id])) {

						// Use cached version
						$field_object = $this->field_object_cache[$field_id];

					} else {

						// Read field data and get type
						$ws_form_field = new WS_Form_Field();
						$ws_form_field->id = $field_id;
						$field_object = $ws_form_field->db_read(true, $bypass_user_capability_check);
						$this->field_object_cache[$field_id] = $field_object;
					}

					// If field no longer exists, just return the value
					if($field_object === false) {

						$submit_meta[$meta['meta_key']] = $value;
						continue;
					}

					// Get field type
					$field_type = $field_object->type;

					// If field type not known, skip
					if($this->field_types === false) { $this->field_types = WS_Form_Config::get_field_types_flat(); }
					if(!isset($this->field_types[$field_type])) { continue; }
					$field_type_config = $this->field_types[$field_type];

					// Legacy date format support
					if(
						($field_type === 'datetime') &&
						is_array($value) &&
						isset($value['mysql'])
					) {
						$value = $value['mysql'];
					}

					// Submit array
					$field_submit_array = (isset($field_type_config['submit_array'])) ? $field_type_config['submit_array'] : false;

					// Build meta key
					$meta_key = is_null($meta['meta_key']) ? (WS_FORM_FIELD_PREFIX . $field_id) : $meta['meta_key'];

					// Check for repeater
					$repeatable_index = (
						isset($meta['repeatable_index']) &&
						(absint($meta['repeatable_index']) > 0)
					) ? absint($meta['repeatable_index']) : false;

					// Check for section_id
					$section_id = (
						isset($meta['section_id']) &&
						(absint($meta['section_id']) > 0)
					) ? absint($meta['section_id']) : false;

					// Check for repeatable_delimiter_section
					$section_key = 'section_' . $section_id;
					$delimiter_section = (
						isset($this->section_repeatable[$section_key]) &&
						isset($this->section_repeatable[$section_key]['delimiter_section'])
					) ? $this->section_repeatable[$section_key]['delimiter_section'] : WS_FORM_SECTION_REPEATABLE_DELIMITER_SECTION;

					// Check for repeatable_delimiter_row
					$delimiter_row = (
						isset($this->section_repeatable[$section_key]) &&
						isset($this->section_repeatable[$section_key]['delimiter_row'])
					) ? $this->section_repeatable[$section_key]['delimiter_row'] : WS_FORM_SECTION_REPEATABLE_DELIMITER_ROW;

					// Sanitize delimiters
					$delimiter_section = WS_Form_Common::delimiter_sanitize($delimiter_section);
					$delimiter_row = WS_Form_Common::delimiter_sanitize($delimiter_row);

					// Build meta data
					$meta_data = array('id' => $field_id, 'value' => $value, 'type' => $field_type, 'section_id' => $section_id, 'repeatable_index' => $repeatable_index);

					// Add to submit meta
					$submit_meta[$meta_key] = $meta_data;

					// Build fallback value
					if($repeatable_index !== false) {

						$meta_key_base = WS_FORM_FIELD_PREFIX . $field_id;

						$submit_meta_not_set = !isset($submit_meta[$meta_key_base]);

						if($submit_meta_not_set) {

							$submit_meta[$meta_key_base] = $meta_data;
						}

						$submit_meta[$meta_key_base]['db_ignore'] = true;
						$submit_meta[$meta_key_base]['repeatable_index'] = false;

						switch($field_type) {

							// Arrays
							case 'file' :
							case 'mediacapture' :
							case 'signature' :
							case 'googlemap' :

								if($submit_meta_not_set) {

									$submit_meta[$meta_key_base]['value'] = $value;

								} else {

									if(is_array($value)) {

										$submit_meta[$meta_key_base]['value'] = is_array($submit_meta[$meta_key_base]['value']) ?  array_merge($submit_meta[$meta_key_base]['value'], $value) : $value;
									}
								}

								break;

							// Strings
							default :

								if($submit_meta_not_set) {

									$submit_meta[$meta_key_base]['value'] = self::field_value_stringify($field_object, $submit_meta[$meta_key_base]['value'], $field_submit_array, $delimiter_row);

								} else {

									$submit_meta[$meta_key_base]['value'] .= $delimiter_section . self::field_value_stringify($field_object, $value, $field_submit_array, $delimiter_row);
								}
						}

						// Store raw array values
						$submit_array = isset($field_type_config['submit_array']) ? $field_type_config['submit_array'] : false;
						if($submit_array) {

							if(!is_array($value)) { $value = array($value); }

							if(!isset($submit_meta[$meta_key_base]['value_array'])) {

								$submit_meta[$meta_key_base]['value_array'] = $value;

							} else {

								$submit_meta[$meta_key_base]['value_array'] = array_merge($submit_meta[$meta_key_base]['value_array'], $value);
							}
						}
					}

				} else {

					// Return as string
					$submit_meta[$meta['meta_key']] = $value;
				}
			}

			return $submit_meta;
		}

		// Get number for form submissions
		public function db_get_count_submit_cached($bypass_user_capability_check = false) {

			self::db_check_form_id();

			// User capability check
			WS_Form_Common::user_must('read_submission', $bypass_user_capability_check);

			// Check cache
			if($this->form_count_submit_cache === false) {

				global $wpdb;

				// Build cache
				$this->form_count_submit_cache = array();

				// Get total number of form submissions
				// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom database table
				$rows = $wpdb->get_results("SELECT form_id, COUNT(id) AS count_submit FROM {$wpdb->prefix}wsf_submit WHERE NOT (status = 'trash') GROUP BY form_id;");

				if(is_null($rows)) { return 0; }

				foreach($rows as $row) {

					$this->form_count_submit_cache[absint($row->form_id)] = absint($row->count_submit);
				}
			}

			return isset($this->form_count_submit_cache[$this->form_id]) ? $this->form_count_submit_cache[$this->form_id] : 0;
		}

		// Get number for form submissions unread
		public function db_get_count_submit_unread_cached($bypass_user_capability_check = false) {

			self::db_check_form_id();

			// User capability check
			WS_Form_Common::user_must('read_submission', $bypass_user_capability_check);

			// Check cache
			if($this->form_count_submit_unread_cache === false) {

				global $wpdb;

				// Build cache
				$this->form_count_submit_unread_cache = array();

				// Get total number of form submissions that are unread
				// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom database table
				$rows = $wpdb->get_results("SELECT form_id, COUNT(id) AS count_submit_unread FROM {$wpdb->prefix}wsf_submit WHERE viewed = 0 AND status IN ('publish', 'draft') GROUP BY form_id;");

				if(is_null($rows)) { return 0; }

				foreach($rows as $row) {

					$this->form_count_submit_unread_cache[absint($row->form_id)] = absint($row->count_submit_unread);
				}
			}

			return isset($this->form_count_submit_unread_cache[$this->form_id]) ? $this->form_count_submit_unread_cache[$this->form_id] : 0;
		}

		// Restore
		public function db_restore($count_update = true) {

			// User capability check
			WS_Form_Common::user_must('delete_submission');

			self::db_set_status('draft', $count_update);
		}

		// Set starred on / off
		public function db_set_starred($starred = true) {

			// User capability check
			WS_Form_Common::user_must('edit_submission');

			self::db_check_id();

			global $wpdb;

			// Update submit record
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom database table
			$update_result = $wpdb->update(
				"{$wpdb->prefix}wsf_submit",
				array(
					'starred' => ($starred ? 1 : 0),
				),
				array( 'id' => $this->id ),
				array( '%d' ),
				array( '%d' )
			);

			if($update_result === false) { 
				parent::db_wpdb_handle_error(__('Error setting starred status.', 'ws-form')); 
			}
		}

		// Set a submit record as viewed
		public function db_set_viewed($viewed = true, $update_count_submit_unread = true) {

			// User capability check
			WS_Form_Common::user_must('read_submission');

			// Check ID
			self::db_check_id();

			global $wpdb;

			// Set viewed true
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom database table
			$update_result = $wpdb->update(
				"{$wpdb->prefix}wsf_submit",
				array(
					'viewed' => ($viewed ? 1 : 0),
				),
				array( 'id' => $this->id ),
				array( '%d' ),
				array( '%d' )
			);

			if($update_result === false) { 
				parent::db_wpdb_handle_error(__('Error updating viewed status.', 'ws-form')); 
			}

			// Update form submit unread count statistic
			if($update_count_submit_unread) {

				$ws_form_form = new WS_Form_Form();
				$ws_form_form->id = $this->form_id;
				$ws_form_form->db_update_count_submit_unread();
			}
		}

		// Set status of submit
		public function db_set_status($status, $count_update = true, $bypass_user_capability_check = false) {

			// User capability check
			WS_Form_Common::user_must('edit_submission', $bypass_user_capability_check);

			self::db_check_id();

			global $wpdb;

			// Mark As Spam
			switch($status) {

				case 'spam' :

					// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom database table
					$records_updated = $wpdb->update(
						"{$wpdb->prefix}wsf_submit",
						array(
							'status' => $status,
							'spam_level' => 100,
						),
						array( 'id' => $this->id ),
						array( '%s', '%d' ),
						array( '%d' )
					);

					break;

				case 'not_spam' :

					$status = 'publish';

					// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom database table
					$records_updated = $wpdb->update(
						"{$wpdb->prefix}wsf_submit",
						array(
							'status' => $status,
							'spam_level' => 0,
						),
						array( 'id' => $this->id ),
						array( '%s', '%d' ),
						array( '%d' )
					);

					break;

				default :

					// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom database table
					$records_updated = $wpdb->update(
						"{$wpdb->prefix}wsf_submit",
						array(
							'status' => $status,
						),
						array( 'id' => $this->id ),
						array( '%s' ),
						array( '%d' )
					);
			}

			// Ensure provided submit status is valid
			if(WS_Form_Common::check_submit_status($status) == '') {

				/* translators: %s: Status */
				parent::db_throw_error(sprintf(__('Invalid submit status: %s', 'ws-form'), $status));
			}

			// Update submit record
			if($records_updated === false) {

				parent::db_wpdb_handle_error(__('Error setting submit status.', 'ws-form'));
			}

			// Update form submit unread count statistic
			if($count_update) {

				self::db_check_form_id();

				$ws_form_form = new WS_Form_Form();
				$ws_form_form->id = $this->form_id;
				$ws_form_form->db_count_update(false, $bypass_user_capability_check);
			}

			// Run action
			// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- All hooks prefixed with wsf_
			do_action('wsf_submit_status', $this->id, $status);

			return true;
		}

		// Get submit status name
		public static function db_get_status_name($status) {

			switch($status) {

				case 'draft' : 		return __('In Progress', 'ws-form'); break;
				case 'publish' : 	return __('Submitted', 'ws-form'); break;
				case 'error' : 		return __('Error', 'ws-form'); break;
				case 'spam' : 		return __('Spam', 'ws-form'); break;
				case 'trash' : 		return __('Trash', 'ws-form'); break;
				default :			return $status;
			}
		}

		// Get submit columns
		public function db_get_submit_fields($bypass_user_capability_check = false) {

			// User capability check
			WS_Form_Common::user_must('read_submission', $bypass_user_capability_check);

			if(is_array($this->submit_fields)) { return $this->submit_fields; }

			self::db_check_form_id();

			$visible_count = 0;
			$visible_count_max = 5;

			$this->submit_fields = array();

			// Get form object
			$this->preview = true;
			self::db_form_object_read($bypass_user_capability_check);

			// Get fields in single dimension array
			$fields = WS_Form_Common::get_fields_from_form($this->form_object);

			// Excluded field types
			$field_types_excluded = array('textarea');

			foreach($fields as $field) {

				if($this->field_types === false) { $this->field_types = WS_Form_Config::get_field_types_flat(); }
				if(!isset($this->field_types[$field->type])) { continue; }

				// Get field type
				$field_type_config = $this->field_types[$field->type];

				// Skip unlicensed fields
				if(
					isset($field_type_config['pro_required']) &&
					$field_type_config['pro_required']

				) { continue; }

				// Skip fields that are not saved to meta data
				if(!$field_type_config['submit_save']) { continue; }

				// Skip fields containing the word 'gdpr'
				if(strpos(strtolower($field->label), 'gdpr') !== false) { continue; }

				// Determine if field is required
				$required = WS_Form_Common::get_object_meta_value($field, 'required', false);

				// Determine excluded fields
				$excluded = in_array($field->type, $field_types_excluded);

				// Push to this->submit_fields array
				$this->submit_fields[$field->id] = array(

					'label' 	=> $field->label,
					'type'		=> $field->type,
					'required' 	=> $required,
					'excluded'	=> $excluded,
					'hidden'	=> true
				);
			}

			// Go through each submit field and if it is required, mark it as not hidden
			foreach($this->submit_fields as $id => $field) {

				if($visible_count < $visible_count_max) {

					if($field['required'] && !$field['excluded']) {

						$this->submit_fields[$id]['hidden'] = false;
						$visible_count++;
					}

					if($visible_count == $visible_count_max) { break; }
				}
			}

			if($visible_count < $visible_count_max) {

				// Go through each submit field and if it is not required, mark it as not hidden
				foreach($this->submit_fields as $id => $field) {

					if($visible_count < $visible_count_max) {

						if(!$field['required'] && !$field['excluded']) {

							$this->submit_fields[$id]['hidden'] = false;
							$visible_count++;
						}

						if($visible_count == $visible_count_max) { break; }
					}
				}
			}

			return $this->submit_fields;
		}

		public function get_select($join = false) {

			// Get form data
			$select = self::DB_SELECT;

			if(!empty($join)) {

				$select_array = explode(',', $select);

				foreach($select_array as $key => $select) {

					$select_array[$key] = $this->table_name . '.' . $select;
				}

				$select = implode(',', $select_array);
			}

			return $select;
		}

		public function get_search_join() {

			// Get keyword
			$keyword = WS_Form_Common::get_query_var('keyword');

			// Get order by column
			$order_by = WS_Form_Common::get_query_var('orderby', '');

			return self::get_join($keyword, $order_by, false);
		}

		public function get_join($keyword = '', $order_by = '', $bypass_user_capability_check = false) {

			global $wpdb;

			// Sanitize inputs
			$keyword = sanitize_text_field(trim($keyword));
			$order_by = sanitize_text_field(trim($order_by));

			$join = array();

			// Process keyword
			if($keyword != '') {

				// Get submit fields so we only search current form fields (Excluding old submit meta data)
				self::db_get_submit_fields($bypass_user_capability_check);

				// Keyword search if fields exists
				if(count($this->submit_fields) > 0) {

					$join[] = sprintf(

						'RIGHT JOIN %1$ssubmit_meta smk ON (smk.parent_id = %1$ssubmit.id) AND (smk.field_id IN (%3$s) AND (smk.meta_value LIKE \'%%%2$s%%\'))',
						$wpdb->prefix . WS_FORM_DB_TABLE_PREFIX,
						esc_sql($keyword),
						esc_sql(implode(',', array_keys($this->submit_fields)))
					);
				}
			}

			// Process order by
			if(!empty($order_by)) {

				switch($order_by) {

					case 'id' :
					case 'starred' :
					case 'status' :
					case 'date_added' :
					case 'date_updated' :

						break;

					default :

						$join[] = sprintf('LEFT OUTER JOIN %1$ssubmit_meta ON (%1$ssubmit_meta.parent_id = %1$ssubmit.id) AND (%1$ssubmit_meta.meta_key = \'%2$s\')', $wpdb->prefix . WS_FORM_DB_TABLE_PREFIX, esc_sql($order_by));
				}
			}

			return implode(' ', $join);
		}

		public function get_search_where() {

			return self::get_where(WS_Form_Common::get_admin_submit_filters());
		}

		public function get_where($filters = false, $bypass_user_capability_check = false) {

			global $wpdb;

			// Build WHERE
			$where = sprintf('form_id = %u', $this->form_id);

			if(empty($filters)) {

				// Default status WHERE SQL
				$where .= self::get_where_default_status();

				return $where;
			}

			// Status set
			$status_set = false;

			// Get keys so we can check valid field values
			$keys_fixed = self::get_keys_fixed($bypass_user_capability_check);
			$keys_meta = self::get_keys_meta($bypass_user_capability_check);		// Sets $this->submit_fields;

			// Process filters
			foreach($filters as $filter) {

				// Sanitize filter
				try {

					$filter = self::filter_sanitize($filter, $bypass_user_capability_check);

				} catch (Exception $e) {

					self::db_throw_error($e->getMessage());
				}

				// Read filter
				$field = $filter['field'];
				$operator = $filter['operator'];
				$value = $filter['value'];

				// Specific field / value combinations
				switch($field) {

					case 'status' :

						if($status_set) { continue 2; }

						// Set status set
						$status_set = true;

						// Process Status
						if($value == 'all') {

							// Default status WHERE SQL
							$where .= self::get_where_default_status();

							continue 2;
						};

						break;
				}

				// Process fixed fields
				if(isset($keys_fixed[$field])) {

					// Build SQL
					switch($operator) {

						case '=' :
						case '==' :
						case '>' :
						case '>=' :
						case '<' :
						case '<=' :

							if($operator == '==') { $operator = '='; }

							$where .= sprintf(

								' AND %s %s "%s"',
								esc_sql($field),
								$operator,
								esc_sql($value)
							);

							break;

						case '!=' :

							$where .= sprintf(

								' AND NOT(%s = "%s")',
								esc_sql($field),
								esc_sql($value)
							);

							break;

						case 'in' :

							$where .= sprintf(

								' AND %s IN(%s)',
								esc_sql($field),
								$value                  // Already escaped by filter_sanitize method
							);

							break;

						case 'not_in' :

							$where .= sprintf(

								' AND %s NOT IN(%s)',
								esc_sql($field),
								$value                  // Already escaped by filter_sanitize method
							);

							break;
					}

					continue;
				}

				// Meta data
				if(isset($keys_meta[$field])) {

					// Get field type
					$field_type = $this->submit_fields[$field]['type'];

					// Build SQL
					switch($operator) {

						case '=' :
						case '==' :
						case '>' :
						case '>=' :
						case '<' :
						case '<=' :

							if($operator == '==') { $operator = '='; }

							$where .= sprintf(

								' AND (NOT (SELECT mks.id FROM %1$ssubmit_meta mks WHERE mks.parent_id =  %1$ssubmit.id AND mks.meta_key = "%2$s" AND mks.meta_value %4$s "%3$s" LIMIT 1) IS NULL)',
								$wpdb->prefix . WS_FORM_DB_TABLE_PREFIX,
								esc_sql($field),
								esc_sql($value),
								$operator
							);

							break;

						case '!=' :

							$where .= sprintf(

								' AND (NOT (SELECT mks.id FROM %1$ssubmit_meta mks WHERE mks.parent_id =  %1$ssubmit.id AND mks.meta_key = "%2$s" AND NOT(mks.meta_value = "%3$s") LIMIT 1) IS NULL)',
								$wpdb->prefix . WS_FORM_DB_TABLE_PREFIX,
								esc_sql($field),
								esc_sql($value)
							);

							break;

						case 'in' :

							$where .= sprintf(

								' AND (NOT (SELECT mks.id FROM %1$ssubmit_meta mks WHERE mks.parent_id =  %1$ssubmit.id AND mks.meta_key = "%2$s" AND mks.meta_value IN(%3$s) LIMIT 1) IS NULL)',
								$wpdb->prefix . WS_FORM_DB_TABLE_PREFIX,
								esc_sql($field),
								$value             // Already escaped by filter_sanitize method
							);

							break;

						case 'not_in' :

							$where .= sprintf(

								' AND (NOT (SELECT mks.id FROM %1$ssubmit_meta mks WHERE mks.parent_id =  %1$ssubmit.id AND mks.meta_key = "%2$s" AND mks.meta_value NOT IN(%3$s) LIMIT 1) IS NULL)',
								$wpdb->prefix . WS_FORM_DB_TABLE_PREFIX,
								esc_sql($field),
								$value             // Already escaped by filter_sanitize method
							);

							break;
					}
				}
			}

			// If status has not been set, add default status WHERE SQL
			if(!$status_set) { $where .= self::get_where_default_status(); }

			return $where;
		}

		public function get_where_default_status() {

			return " AND NOT(status = 'trash' OR status = 'spam')";
		}

		public function filter_sanitize($filter, $bypass_user_capability_check) {

			// Get field
			if(!isset($filter['field'])) { self::db_throw_error(__('No filter field specified.', 'ws-form')); }
			$filter['field'] = sanitize_text_field($filter['field']);

			// Convert date_from field
			if($filter['field'] == 'date_from') {

				$filter['field'] = 'date_added';
				$filter['operator'] = '>=';
				$filter['value'] = WS_Form_Common::get_mysql_date(get_gmt_from_date(WS_Form_Common::get_date_by_site(WS_Form_Common::field_date_translate($filter['value'])) . ' 00:00:00'));
			}

			// Convert date_to field
			if($filter['field'] == 'date_to') {

				$filter['field'] = 'date_added';
				$filter['operator'] = '<=';
				$filter['value'] = WS_Form_Common::get_mysql_date(get_gmt_from_date(WS_Form_Common::get_date_by_site(WS_Form_Common::field_date_translate($filter['value'])) . ' 23:59:59'));
			}

			// Check field
			$keys_all = self::get_keys_all($bypass_user_capability_check);
			if(!isset($keys_all[$filter['field']])) { self::db_throw_error(__('Invalid filter field specified.', 'ws-form')); }

			// Get value
			if(!isset($filter['value'])) { self::db_throw_error(__('No filter value specified.', 'ws-form')); }

			// Check value
			if($filter['value'] == '') { self::db_throw_error(__('Empty filter value specified.', 'ws-form')); }
			if(
				!is_string($filter['value']) &&
				!is_numeric($filter['value']) &&
				!is_array($filter['value'])
			) {
				self::db_throw_error(__('Invalid value variable type.', 'ws-form'));
			}

			// Check operator
			$filter['operator'] = strtolower(isset($filter['operator']) ? $filter['operator'] : '==');
			if(!in_array($filter['operator'], array('==', '!=', '>', '>=', '<', '<=', 'in', 'not_in'))) {

				self::db_throw_error(__('Invalid operator specified.', 'ws-form'));
			}

			// Specific field checks
			switch($filter['field']) {

				case 'status' :

					if(WS_Form_Common::check_submit_status($filter['value']) == '') { $filter['value'] = 'all'; }
					break;
			}

			// Operator checks
			switch($filter['operator']) {

				case 'in' :
				case 'not_in' :

					if(!is_array($filter['value'])) { self::db_throw_error(__('Value should be an array.', 'ws-form')); }

					// Process and escape each value
					$value_array_escaped = array();

					foreach($filter['value'] as $value_single) {

						switch(gettype($value_single)) {

							case 'string' :

								$value_array_escaped[] = sprintf('"%s"', esc_sql($value_single));
								break;

							case 'integer' :

								$value_array_escaped[] = esc_sql(intval($value_single));
								break;

							case 'double' :

								$value_array_escaped[] = esc_sql(floatval($value_single));
								break;

							default :

								self::db_throw_error(__('Invalid value variable type.', 'ws-form'));
						}
					}

					$filter['value'] = implode(',', $value_array_escaped);

					break;

				default :

					if(is_array($filter['value'])) { self::db_throw_error(__('Value should not be an array.', 'ws-form')); }

					$filter['value'] = sanitize_text_field($filter['value']);
			}

			return $filter;
		}

		public function get_search_group_by() {

			return self::get_group_by();
		}

		public function get_group_by() {

			// Build GROUP BY SQL
			return sprintf(

				'%s.id',
				$this->table_name
			);
		}

		public function get_search_order_by() {

			// Get order by
			$order_by = WS_Form_Common::get_query_var('orderby');

			// Get order
			$order = WS_Form_Common::get_query_var('order');

			return self::get_order_by($order_by, $order, false);
		}

		public function get_order_by($order_by = 'id', $order = 'DESC', $bypass_user_capability_check = false) {

			global $wpdb;

			// Sanitize inputs
			$order = strtolower(sanitize_text_field(trim($order)));
			$order_by = strtolower(sanitize_text_field(trim($order_by)));

			// Default
			$order_by_return = 'id DESC';

			// Process order by
			if(!empty($order_by)) {

				// Process order
				$order = (!empty($order) && ($order == 'desc')) ? ' DESC' : ' ASC';

				switch($order_by) {

					case 'id' :
					case 'starred' :
					case 'status' :
					case 'date_added' :
					case 'date_updated' :

						$order_by_return = esc_sql($order_by) . $order;
						break;

					default :

						// Get field ID
						$field_id = absint(str_replace(WS_FORM_FIELD_PREFIX, '', $order_by));

						// Not a recognized column or field, fall back to safe default (prevents SQL injection via unescaped ORDER BY)
						if($field_id == 0) { return $order_by_return; }

						// Read field object
						try {

							$ws_form_field = new WS_Form_Field();
							$ws_form_field->id = $field_id;
							$field_obj = $ws_form_field->db_read(true, $bypass_user_capability_check);
							if(empty($field_obj)) { return $order_by_return; }

						} catch (Exception $e) {

							return $order_by_return;
						}

						// Process by field object type
						if($field_obj && property_exists($field_obj, 'type')) {

							switch($field_obj->type) {

								case 'select' :
								case 'checkbox' :
								case 'radio' :

									// Select, checkbox, radio
									$order_by_meta_value = sprintf('(TRIM(BOTH \'"\' FROM SUBSTRING_INDEX(SUBSTRING_INDEX(%1$ssubmit_meta.meta_value,\';\',2),\':\',-1)))', $wpdb->prefix . WS_FORM_DB_TABLE_PREFIX);
									break;

								case 'datetime' :

									// Date
									$format_date = WS_Form_Common::get_object_meta_value($field_obj, 'format_date', get_option('date_format'));
									if(empty($format_date)) { $format_date = get_option('date_format'); }
									$format_date = WS_Form_Common::php_to_mysql_date_format($format_date);

									$format_time = WS_Form_Common::get_object_meta_value($field_obj, 'format_time', get_option('time_format'));
									if(empty($format_time)) { $format_time = get_option('time_format'); }
									$format_time = WS_Form_Common::php_to_mysql_date_format($format_time);

									$input_type_datetime = WS_Form_Common::get_object_meta_value($field_obj, 'input_type_datetime', 'date');

									switch($input_type_datetime) {

										case 'date' :

											$format_string = $format_date;
											break;

										case 'month' :

											$format_string = '%M %Y';
											break;

										case 'time' :

											$format_string = $format_time;
											break;

										case 'week' :

											$format_string = __('Week', 'ws-form') . '%u, %Y';
											break;

										default :

											$format_string = $format_date . ' ' . $format_time;
									}

									$order_by_meta_value = sprintf("STR_TO_DATE(%ssubmit_meta.meta_value, '%s')", $wpdb->prefix . WS_FORM_DB_TABLE_PREFIX, $format_string);
									break;

								case 'price' :

									// Price
									$order_by_meta_value = '(SUBSTRING(' . $wpdb->prefix . WS_FORM_DB_TABLE_PREFIX . 'submit_meta.meta_value, 2) * 1)';
									break;

								case 'number' :
								case 'range' :

									// Number
									$order_by_meta_value = '(' . $wpdb->prefix . WS_FORM_DB_TABLE_PREFIX . 'submit_meta.meta_value * 1)';
									break;

								default :

									// Default
									$order_by_meta_value = $wpdb->prefix . WS_FORM_DB_TABLE_PREFIX . 'submit_meta.meta_value';
							}

							$order_by_return = $order_by_meta_value . $order;
						}
				}
			}

			return $order_by_return;
		}

		// Get keys - All
		public function get_keys_all($bypass_user_capability_check = false) {

			// User capability check
			WS_Form_Common::user_must('read_submission', $bypass_user_capability_check);

			// Check cache
			if($this->keys !== false) { return $this->keys; }

			// Add keys - Fixed
			$this->keys = self::get_keys_fixed($bypass_user_capability_check);

			// Add keys - Fields
			$this->keys = array_merge($this->keys, self::get_keys_fields($bypass_user_capability_check));


			return $this->keys;
		}

		// Get keys - Meta
		public function get_keys_meta($bypass_user_capability_check = false) {

			// User capability check
			WS_Form_Common::user_must('read_submission', $bypass_user_capability_check);

			// Check cache
			if($this->keys_meta !== false) { return $this->keys_meta; }

			// Add keys - Fields
			$this->keys_meta = self::get_keys_fields($bypass_user_capability_check);


			return $this->keys_meta;
		}

		// Get keys - Fixed
		public function get_keys_fixed($bypass_user_capability_check = false) {

			// User capability check
			WS_Form_Common::user_must('read_submission', $bypass_user_capability_check);

			// Check cache
			if($this->keys_fixed !== false) { return $this->keys_fixed; }

			// Build fixed keys fields
			$this->keys_fixed = array(

				'id' => 'Submission ID',
				'status' => 'Status',
				'status_full' => 'Status Full',
				'date_added' => 'Date Added',
				'date_updated' => 'Date Updated',
				'user_id' => 'User ID',
				'user_first_name' => 'User First Name',
				'user_last_name' => 'User Last Name',
				'duration' => 'Duration (Seconds)'
			);

			return $this->keys_fixed;
		}

		// Get keys - Fields
		public function get_keys_fields($bypass_user_capability_check = false) {

			// User capability check
			WS_Form_Common::user_must('read_submission', $bypass_user_capability_check);

			// Check cache
			if($this->keys_fields !== false) { return $this->keys_fields; }

			// Get field data
			$submit_fields = self::db_get_submit_fields($bypass_user_capability_check);

			// Build keys
			$this->keys_fields = array();

			// Form fields
			if(is_array($submit_fields)) {

				foreach($submit_fields as $id => $submit_field) {

					$this->keys_fields[sprintf('field_%u', $id)] = $submit_field['label'];
				}
			}

			return $this->keys_fields;
		}

		// Get e-commerce keys
		public function get_keys_ecommerce($bypass_user_capability_check = false) {

			// User capability check
			WS_Form_Common::user_must('read_submission', $bypass_user_capability_check);

			// Check cache
			if($this->keys_ecommerce !== false) { return $this->keys_ecommerce; }

			$this->keys_ecommerce = array();

			// E-Commerce
			$ecommerce = WS_Form_Config::get_ecommerce(false);

			// Builder keys columns according to priority
			$keys_ecommerce = array();
			foreach($ecommerce['cart_price_types'] as $key => $ecommerce_config) {

				$priority = isset($ecommerce_config['priority']) ? $ecommerce_config['priority'] : 10000;
				$keys_ecommerce[$key] = array('key' => 'ecommerce_cart_' . $key, 'label' => $ecommerce_config['label'], 'priority' => $priority);
			}
			foreach($ecommerce['meta_keys'] as $key => $ecommerce_config) {

				$priority = isset($ecommerce_config['priority']) ? $ecommerce_config['priority'] : 10000;
				$keys_ecommerce[$key] = array('key' => $key, 'label' => $ecommerce_config['label'], 'priority' => $priority);
			}
			uasort($keys_ecommerce, function ($keys_ecommerce_1, $keys_ecommerce_2) {

				return ($keys_ecommerce_1['priority'] == $keys_ecommerce_2['priority']) ? 0 : (($keys_ecommerce_1['priority'] < $keys_ecommerce_2['priority']) ? -1 : 1);
			});
			foreach($keys_ecommerce as $keys_ecommerce_single) {

				$this->keys_ecommerce[$keys_ecommerce_single['key']] = $keys_ecommerce_single['label'];
			}

			return $this->keys_ecommerce;
		}

		// Get keys - Tracking
		public function get_keys_tracking($bypass_user_capability_check = false) {

			// User capability check
			WS_Form_Common::user_must('read_submission', $bypass_user_capability_check);

			// Check cache
			if($this->keys_tracking !== false) { return $this->keys_tracking; }

			$this->keys_tracking = array();

			// Tracking
			$tracking = WS_Form_Config::get_tracking(false);
			foreach($tracking as $key => $tracking_config) {

				$this->keys_tracking[$key] = $tracking_config['label'];
			}

			return $this->keys_tracking;
		}

		// Setup from post
		public function setup_from_post($process_file_fields = true, $process_non_file_fields = true) {

			// No capabilities required, this is a public method

			// Get form ID
			$this->form_id = absint(WS_Form_Common::get_query_var_nonce('wsf_form_id', 0));

			// If form ID is not specified then we should stop processing
			if($this->form_id === 0) { exit; }

			// Get hash
			$this->hash = WS_Form_Common::get_query_var_nonce('wsf_hash', '');

			// If hash found, look for form submission
			if(
				($this->hash != '') &&
				WS_Form_Common::check_submit_hash($this->hash)
			) {
				try {

					// Read submit by hash
					$this->db_read_by_hash(true, true, true, true);

					// Reset spam level
					$this->spam_level = null;

					// Clear meta data
					// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- All hooks prefixed with wsf_
					$submit_clear_meta_filter_keys = apply_filters('wsf_submit_clear_meta_filter_keys', array());
					foreach($this->meta as $key => $value) {

						if(!in_array($key, $submit_clear_meta_filter_keys)) {

							unset($this->meta[$key]);
						}
					}
					$this->meta_protected = array();

				} catch(Exception $e) {

					$this->hash = '';
				}
			}

			if($this->hash == '') {

				// Create fresh hash for this submission
				$this->db_create_hash();
			}

			// Preview submit?
			$this->preview = (WS_Form_Common::get_query_var_nonce('wsf_preview', false) !== false);

			// Read form
			try {
	
				self::db_form_object_read();

			} catch(Exception $e) {

				self::return_forbidden();
			}
	
			// Apply restrictions (Removes any groups, sections or fields that are hidden due to restriction settings, e.g. User logged in)
			$ws_form_form = new WS_Form_Form();
			$ws_form_form->apply_restrictions($this->form_object);

			// Apply limits
			if(!$this->preview) {

				$check_limit_response = $ws_form_form->apply_limits($this->form_object);
				if($check_limit_response !== false) {

					self::return_forbidden();
				}
			}

			// Build keyword blocklist
			$keyword_blocklist = array();

			// Check if 
			if(WS_Form_Common::get_object_meta_value($this->form_object, 'keyword_blocklist', '')) {

				// Check limit count
				$keyword_blocklist_keywords = WS_Form_Common::get_object_meta_value($this->form_object, 'keyword_blocklist_keywords', '');
				if(is_array($keyword_blocklist_keywords)) {

					foreach($keyword_blocklist_keywords as $row) {

						if(!isset($row->keyword_blocklist_keyword)) { continue; }

						$keyword_blocklist[] = $row->keyword_blocklist_keyword;
					}
				}

				// Sanitize before filter
				$keyword_blocklist = WS_Form_Common::sanitize_keyword_array($keyword_blocklist);
			}

			// Apply filters
			// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- All hooks prefixed with wsf_
			$keyword_blocklist = apply_filters('wsf_submit_block_keywords', $keyword_blocklist, $this->form_object, $this);

			// Sanitize after filter
			$keyword_blocklist = WS_Form_Common::sanitize_keyword_array($keyword_blocklist);

			// Do not validate fields that are required bypassed
			$bypass_required = WS_Form_Common::get_query_var_nonce('wsf_bypass_required', '');
			$this->bypass_required_array = explode(',', $bypass_required);

			// Process hidden fields
			$hidden = WS_Form_Common::get_query_var_nonce('wsf_hidden', '');
			$this->hidden_array = explode(',', $hidden);
			if(count($this->hidden_array) > 0) {

				$this->meta['wsf_meta_key_hidden'] = array();
			}

			// Spam protection - Honeypot
			$honeypot_hash = ($this->form_object->published_checksum != '') ? $this->form_object->published_checksum : 'honeypot_unpublished_' . $this->form_id;
			$honeypot_value = WS_Form_Common::get_query_var_nonce("field_$honeypot_hash");
			if($honeypot_value != '') {

				self::return_forbidden();
			}

			// Get sections array
			$sections = WS_Form_Common::get_sections_from_form($this->form_object);

			// Are we submitting the form or just saving it?
			$this->post_mode = WS_Form_Common::get_query_var_nonce('wsf_post_mode', false);
			$form_submit = ($this->post_mode == 'submit');

			// Ensure post mode is valid
			if(!in_array($this->post_mode, array('submit', 'save', 'action'))) {

				self::return_forbidden();
			}
			// Build section_repeatable
			$section_repeatable = array();
			$wsf_form_section_repeatable_index_json = WS_Form_Common::get_query_var_nonce('wsf_form_section_repeatable_index', false);
			if(!empty($wsf_form_section_repeatable_index_json)) {

				if(is_null($wsf_form_section_repeatable_index = (array) json_decode($wsf_form_section_repeatable_index_json))) {

					parent::db_throw_error(__('Malformed wsf_form_section_repeatable_index JSON value.', 'ws-form'));
				}

				// Save wsf_form_section_repeatable_index to section_repeatable and parse it to ensure the data is valid
				foreach($wsf_form_section_repeatable_index as $section_id_string => $indexes) {

					$section_repeatable[$section_id_string] = array('index' => array());

					foreach($indexes as $index) {

						if(absint($index) === 0) { continue; }

						$section_repeatable[$section_id_string]['index'][] = absint($index);
					}
				}
			}

			// Process each section
			foreach($sections as $section_id => $section) {

				if($section->repeatable) {

					$section_id_string = 'section_' . $section_id;

					// Get repeatable indexes for that section
					if(
						!isset($section_repeatable[$section_id_string]) ||
						!isset($section_repeatable[$section_id_string]['index'])
					) {

						parent::db_throw_error(__('Repeatable data error. Section ID not found in wsf_form_section_repeatable_index.', 'ws-form'));
					}

					$section_repeatable_indexes = $section_repeatable[$section_id_string]['index'];

					foreach($section_repeatable_indexes as $section_repeatable_index) {

						self::setup_from_post_section($section, $form_submit, $process_file_fields, $process_non_file_fields, $keyword_blocklist, $section_id, $section_repeatable_index, $section_repeatable);
					}

				} else {

					self::setup_from_post_section($section, $form_submit, $process_file_fields, $process_non_file_fields, $keyword_blocklist);
				}
			}

			// Apply wsf_submit_validate hook
			$this->error_validation_actions = self::filter_validate(

				$this->error_validation_actions,

				// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- All hooks prefixed with wsf_
				apply_filters('wsf_submit_validate', $this->error_validation_actions, $this->post_mode, $this)
			);

			// Section repeatable
			if(!empty($section_repeatable)) {

				$this->section_repeatable = serialize($section_repeatable);
			}

			// Post ID
			$post_id = absint(WS_Form_Common::get_query_var_nonce('wsf_post_id', 0));
			if($post_id > 0) {

				$this->meta['post_id'] = $post_id;
			}
		}

		public function return_forbidden() {

			// Exit with 403 HTTP status code
			header('HTTP/1.0 403 Forbidden');
			exit;
		}

		public function setup_from_post_section($section, $form_submit, $process_file_fields = true, $process_non_file_fields = true, $keyword_blocklist = array(), $section_id = false, $section_repeatable_index = false, &$section_repeatable = array()) {

			// Delimiters
			if($section_repeatable_index !== false) {

				// Get delimiters
				$delimiter_section = WS_Form_Common::get_object_meta_value($section, 'section_repeatable_delimiter_section', WS_FORM_SECTION_REPEATABLE_DELIMITER_SECTION);
				if($delimiter_section == '') { $delimiter_section = WS_FORM_SECTION_REPEATABLE_DELIMITER_SECTION; }

				$delimiter_row = WS_Form_Common::get_object_meta_value($section, 'section_repeatable_delimiter_row', WS_FORM_SECTION_REPEATABLE_DELIMITER_ROW);
				if($delimiter_row == '') { $delimiter_row = WS_FORM_SECTION_REPEATABLE_DELIMITER_ROW; }

				// Sanitize delimiters
				$delimiter_section = WS_Form_Common::delimiter_sanitize($delimiter_section);
				$delimiter_row = WS_Form_Common::delimiter_sanitize($delimiter_row);

				// Add delimiters to section_repeatable
				if(!isset($section_repeatable['section_' . $section_id])) { $section_repeatable['section_' . $section_id] = array(); }
				$section_repeatable['section_' . $section_id]['delimiter_section'] = $delimiter_section;
				$section_repeatable['section_' . $section_id]['delimiter_row'] = $delimiter_row;
			}

			// File field types
			$field_type_files = array('file', 'mediacapture', 'signature');

			// Process each field
			$section_fields = $section->fields;
			foreach($section_fields as $field) {

				// If field type not specified, skip
				if(!isset($field->type)) { continue; };
				$field_type = $field->type;

				// File processing?
				if(!$process_non_file_fields && !in_array($field_type, $field_type_files)) { continue; }
				if(!$process_file_fields && in_array($field_type, $field_type_files)) { continue; }

				// If field type not known, skip
				if($this->field_types === false) { $this->field_types = WS_Form_Config::get_field_types_flat(); }
				if(!isset($this->field_types[$field_type])) { continue; };
				$field_type_config = $this->field_types[$field_type];

				// Remove layout editor only fields
				$layout_editor_only = isset($field_type_config['layout_editor_only']) ? $field_type_config['layout_editor_only'] : false;
				if($layout_editor_only) { continue; }

				// Submit array
				$submit_array = isset($field_type_config['submit_array']) ? $field_type_config['submit_array'] : false;

				// If field is not licensed, skip
				if(
					isset($field_type_config['pro_required']) &&
					$field_type_config['pro_required']

				) { continue; }

				// Submit array
				$field_submit_array = (isset($field_type_config['submit_array'])) ? $field_type_config['submit_array'] : false; 

				// Is field in a repeatable section?
				$field_section_repeatable = isset($field->section_repeatable) && $field->section_repeatable;

				// Save meta data
				if(!isset($field->id)) { continue; }
				$field_id = absint($field->id);

				// Build field name
				$field_name = $field_name_post = $meta_key_hidden = WS_FORM_FIELD_PREFIX . $field_id;

				// Field value
				$field_value = WS_Form_Common::get_query_var_nonce($field_name);

				if($section_repeatable_index !== false) {

					$field_value = isset($field_value[$section_repeatable_index]) ? $field_value[$section_repeatable_index] : '';
					$field_name_post = sprintf('%s[%u]', $field_name, $section_repeatable_index);
					$meta_key_hidden = sprintf('%s_%u', $field_name, $section_repeatable_index);
				}

				// Filter serialized strings
				if(is_serialized($field_value)) { $field_value = ''; }

				// Field bypassed
				$field_bypassed = in_array($field_name_post, $this->bypass_required_array);

				// Field required
				$field_required = (

					WS_Form_Common::get_object_meta_value($field, 'required', false) &&
					!$field_bypassed &&
					$field_type_config['has_required'] // Set in WS_Form_Config::get_field_types_flat()
				);

				// Process according to field type
				switch($field_type) {

					case 'email' :

						// Build email array
						$field_value_array = (WS_Form_Common::get_object_meta_value($field, 'multiple_email', '') == 'on') ? explode(',', $field_value) : array($field_value);

						// Sanitized array
						$field_value_array_sanitized = array();

						foreach($field_value_array as $field_value) {

							// Sanitize email address
							$field_value = sanitize_email($field_value);

							// Skip empty values
							if(empty($field_value)) { continue; }

							// Double check the email address
							if(!filter_var($field_value, FILTER_VALIDATE_EMAIL)) {

								self::db_throw_error_field_invalid_feedback($field_id, $section_repeatable_index, $email_validate);
								continue 3;
							}

							// Run wsf_action_email_email_validate filter hook
							// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- All hooks prefixed with wsf_
							$email_validate = apply_filters('wsf_action_email_email_validate', true, $field_value, $this->form_object->id, $field_id);

							if(is_string($email_validate)) {

								self::db_throw_error_field_invalid_feedback($field_id, $section_repeatable_index, $email_validate);
								continue 3;
							}

							if($email_validate === false) {

								self::db_throw_error_field_invalid_feedback($field_id, $section_repeatable_index, __('Invalid email address.', 'ws-form'));
								continue 3;
							}

							$field_value_array_sanitized[] = $field_value;
						}

						// Rebuild field value
						$field_value = implode(',', $field_value_array_sanitized);

						break;

					case 'recaptcha' :

						// Only process if form is being submitted
						if($form_submit) {

							// Get reCAPTCHA secret
							$recaptcha_secret_key = WS_Form_Common::get_object_meta_value($field, 'recaptcha_secret_key', '');

							// If field setting is blank, check global setting
							if(empty($recaptcha_secret_key)) {

								$recaptcha_secret_key = WS_Form_Common::option_get('recaptcha_secret_key', '');
							}

							// Process reCAPTCHA
							try {

								self::db_captcha_process($field_id, $section_repeatable_index, $recaptcha_secret_key, WS_FORM_RECAPTCHA_ENDPOINT, WS_FORM_RECAPTCHA_QUERY_VAR);

							} catch (Exception $e) {

								self::db_throw_error_field_invalid_feedback($field_id, $section_repeatable_index, $e->getMessage());
							}
						}

						break;

					case 'hcaptcha' :

						// Only process if form is being submitted
						if($form_submit) {

							// Get hCaptcha secret
							$hcaptcha_secret_key = WS_Form_Common::get_object_meta_value($field, 'hcaptcha_secret_key', '');

							// If field setting is blank, check global setting
							if(empty($hcaptcha_secret_key)) {

								$hcaptcha_secret_key = WS_Form_Common::option_get('hcaptcha_secret_key', '');
							}

							// Process hCaptcha
							try {

								self::db_captcha_process($field_id, $section_repeatable_index, $hcaptcha_secret_key, WS_FORM_HCAPTCHA_ENDPOINT, WS_FORM_HCAPTCHA_QUERY_VAR);

							} catch (Exception $e) {

								self::db_throw_error_field_invalid_feedback($field_id, $section_repeatable_index, $e->getMessage());
							}
						}

						break;

					case 'turnstile' :

						// Only process if form is being submitted
						if($form_submit) {

							// Get Turnstile secret
							$turnstile_secret_key = WS_Form_Common::get_object_meta_value($field, 'turnstile_secret_key', '');

							// If field setting is blank, check global setting
							if(empty($turnstile_secret_key)) {

								$turnstile_secret_key = WS_Form_Common::option_get('turnstile_secret_key', '');
							}

							// Process Turnstile
							try {

								self::db_captcha_process($field_id, $section_repeatable_index, $turnstile_secret_key, WS_FORM_TURNSTILE_ENDPOINT, WS_FORM_TURNSTILE_QUERY_VAR);

							} catch (Exception $e) {

								self::db_throw_error_field_invalid_feedback($field_id, $section_repeatable_index, $e->getMessage());
							}
						}

						break;

					case 'captchafox' :

						// Only process if form is being submitted
						if($form_submit) {

							// Get CaptchaFox secret
							$captchafox_secret_key = WS_Form_Common::get_object_meta_value($field, 'captchafox_secret_key', '');

							// If field setting is blank, check global setting
							if(empty($captchafox_secret_key)) {

								$captchafox_secret_key = WS_Form_Common::option_get('captchafox_secret_key', '');
							}

							// Process CaptchaFox
							try {

								self::db_captcha_process($field_id, $section_repeatable_index, $captchafox_secret_key, WS_FORM_CAPTCHAFOX_ENDPOINT, WS_FORM_CAPTCHAFOX_QUERY_VAR);

							} catch (Exception $e) {

								self::db_throw_error_field_invalid_feedback($field_id, $section_repeatable_index, $e->getMessage());
							}
						}

						break;

					case 'url' :

						$field_value = sanitize_url($field_value);
						break;

					case 'tel' :

						$field_value = WS_Form_Common::sanitize_tel($field_value);
						break;
				}

				// Handle required fields
				if($form_submit && $field_required && ($field_value == '')) {

					self::db_throw_error_field_invalid_feedback($field_id, $section_repeatable_index, self::field_invalid_feedback($field, $field_type_config));
				}

				// Handle hidden fields
				if(in_array($field_name_post, $this->hidden_array)) {

					$this->meta['wsf_meta_key_hidden'][] = $meta_key_hidden;
				}

				// Deduplication
				if($field_value != '') {

					$field_dedupe = WS_Form_Common::get_object_meta_value($field, 'dedupe', false);
					if($field_dedupe) {

						// Get dedupe period
						$field_dedupe_period = WS_Form_Common::get_object_meta_value($field, 'dedupe_period', false);

						// Check for a dupe
						$ws_form_submit_meta = new WS_Form_Submit_Meta();
						if($ws_form_submit_meta->db_dupe_check($this->form_id, $field_id, $field_value, $field_dedupe_period)) {

							$field_dedupe_message = WS_Form_Common::get_object_meta_value($field, 'dedupe_message', '');
							if($field_dedupe_message == '') {

								$field_dedupe_message = __('The value entered has already been used.', 'ws-form');
							}

							$field_dedupe_message_lookups = array(

								'label_lowercase' 	=> strtolower($field->label),
								'label' 			=> $field->label
							);

							$field_dedupe_message = WS_Form_Common::mask_parse($field_dedupe_message, $field_dedupe_message_lookups);

							self::db_throw_error_field_invalid_feedback($field_id, $section_repeatable_index, $field_dedupe_message);
						}
					}
				}

				// Allow / Deny
				$field_allow_deny = WS_Form_Common::get_object_meta_value($field, 'allow_deny', '');
				if(
					($field_allow_deny !== '') &&
					in_array($field_allow_deny, array('allow', 'deny'))
				) {

					$field_value_allowed = ($field_allow_deny === 'deny');

					$field_allow_deny_values = WS_Form_Common::get_object_meta_value($field, 'allow_deny_values', array());

					if(

						is_array($field_allow_deny_values) &&
						(count($field_allow_deny_values) > 0)
					) {

						foreach($field_allow_deny_values as $field_allow_deny_row) {

							$field_allow_deny_value = $field_allow_deny_row->allow_deny_value;

							$field_allow_deny_pattern = str_replace('*', '.*', $field_allow_deny_value);

							$field_allow_deny_result = preg_match(sprintf('/%s/', $field_allow_deny_pattern), $field_value);

							if($field_allow_deny_result) {

								$field_value_allowed = ($field_allow_deny === 'allow');
								break;
							}
						}

						if(!$field_value_allowed) {

							$field_allow_deny_message = WS_Form_Common::get_object_meta_value($field, 'allow_deny_message', '');
							if($field_allow_deny_message == '') {

								$field_allow_deny_message = __('The email address entered is not allowed.', 'ws-form');
							}

							self::db_throw_error_field_invalid_feedback($field_id, $section_repeatable_index, $field_allow_deny_message);
						}
					}
				}

				// Get submit_save
				$submit_save = isset($field_type_config['submit_save']) ? $field_type_config['submit_save'] : false;

				// Build meta_data
				$meta_data = array('id' => $field_id, 'value' => $field_value, 'type' => $field_type, 'section_id' => $section_id, 'repeatable_index' => $section_repeatable_index);
				$meta_key_suffix = (($section_repeatable_index !== false) ? ('_' . $section_repeatable_index) : '');
				if($submit_save !== false) {

					$meta_field = 'meta';

				} else {

					$meta_field = 'meta_protected';
				}

				// Add to submit meta protected
				$this->{$meta_field}[WS_FORM_FIELD_PREFIX . $field_id . $meta_key_suffix] = $meta_data;

				// Build fallback value
				if($section_repeatable_index !== false) {

					$meta_not_set = !isset($this->{$meta_field}[WS_FORM_FIELD_PREFIX . $field_id]);

					if($meta_not_set) {

						$this->{$meta_field}[WS_FORM_FIELD_PREFIX . $field_id] = $meta_data;

						// We don't store the fallback data to the database, it is just made available to any actions that need it
						$this->{$meta_field}[WS_FORM_FIELD_PREFIX . $field_id]['db_ignore'] = true;

						// Set repeatable index to false
						$this->{$meta_field}[WS_FORM_FIELD_PREFIX . $field_id]['repeatable_index'] = false;
					}

					switch($field_type) {

						// Merge
						case 'file' :
						case 'mediacapture' :
						case 'signature' :
						case 'googlemap' :
						case 'select' :
						case 'checkbox' :
						case 'radio' :
						case 'price_select' :
						case 'price_checkbox' :
						case 'price_radio' :

							if($meta_not_set) {

								$this->{$meta_field}[WS_FORM_FIELD_PREFIX . $field_id]['value'] = $field_value;

							} else {

								if(is_array($field_value)) {

									$this->{$meta_field}[WS_FORM_FIELD_PREFIX . $field_id]['value'] = is_array($this->{$meta_field}[WS_FORM_FIELD_PREFIX . $field_id]['value']) ? array_merge($this->{$meta_field}[WS_FORM_FIELD_PREFIX . $field_id]['value'], $field_value) : $field_value;
								}
							}

							break;

						// Other fields
						default :

							if($meta_not_set) {

								$this->{$meta_field}[WS_FORM_FIELD_PREFIX . $field_id]['value'] = self::field_value_stringify($field, $this->{$meta_field}[WS_FORM_FIELD_PREFIX . $field_id]['value'], $field_submit_array, $delimiter_row);

							} else {

								$this->{$meta_field}[WS_FORM_FIELD_PREFIX . $field_id]['value'] .= $delimiter_section . self::field_value_stringify($field, $field_value, $field_submit_array, $delimiter_row);
							}
					}

					// Store raw array values
					if($submit_array) {

						if(!is_array($field_value)) { $field_value = array($field_value); }

						if($meta_not_set) {

							$this->{$meta_field}[WS_FORM_FIELD_PREFIX . $field_id]['value_array'] = $field_value;

						} else {

							$this->{$meta_field}[WS_FORM_FIELD_PREFIX . $field_id]['value_array'] = array_merge($this->{$meta_field}[WS_FORM_FIELD_PREFIX . $field_id]['value_array'], $field_value);
						}
					}
				}

				// Check if field is mappable
				if(isset($field_type_config['mappable']) ? $field_type_config['mappable'] : false) {

					// Apply wsf_submit_field_validate filter hook
					$this->error_validation_actions = self::filter_validate(

						$this->error_validation_actions,

						// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- All hooks prefixed with wsf_
						apply_filters('wsf_submit_field_validate', $this->error_validation_actions, $field_id, $field_value, $section_repeatable_index, $this->post_mode, $this),

						$field,

						$field_id,

						$field_type_config,

						$section_repeatable_index
					);

					// Keyword blocklist
					if(
						is_array($keyword_blocklist) &&
						(count($keyword_blocklist) > 0) &&
						is_string($field_value)
					) {

						// Process keyword list
						$keyword_match = self::keyword_blocklist_check($field_value, $keyword_blocklist);
						if($keyword_match !== false) {

							// Get message
							$keyword_blocklist_message = apply_filters(

								// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- All hooks prefixed with wsf_
								'wsf_submit_block_keywords_message',

								(WS_Form_Common::get_object_meta_value($this->form_object, 'keyword_blocklist', '') ? WS_Form_Common::get_object_meta_value($this->form_object, 'keyword_blocklist_message', '') : ''),

								$this->form_object,

								$this,

								$keyword_match
							);

							// If message is invalid or blank, return false to trigger default invalid feedback
							if(
								!is_string($keyword_blocklist_message) ||
								($keyword_blocklist_message == '')
							) {

								$keyword_blocklist_message = false;

							} else {

								// Parse
								$keyword_blocklist_message_lookups = array(

									'keyword' 	=> $keyword_match
								);

								$keyword_blocklist_message = WS_Form_Common::mask_parse($keyword_blocklist_message, $keyword_blocklist_message_lookups);
							}

							// Apply wsf_submit_field_validate filter hook
							$this->error_validation_actions = self::filter_validate(

								$this->error_validation_actions,

								array($keyword_blocklist_message),

								$field,

								$field_id,

								$field_type_config,

								$section_repeatable_index
							);

							break;
						}
					}
				}
			}
		}

		// Blocked keyword matching
		public function keyword_blocklist_check($text, $keyword_blocklist) {

			// Normalize text
			$normalized = strtolower($text);

			// Replace common obfuscation characters with spaces or remove them
			$normalized = preg_replace('/[\.\,\(\)\[\]\{\}_\-]+/', ' ', $normalized);

			// Collapse multiple spaces into one
			$normalized = preg_replace('/\s+/', ' ', $normalized);

			// Pad the string with spaces for whole word matching
			$normalized = ' ' . $normalized . ' ';

			// Process keyword_blocklist (Already sanitized)
			foreach($keyword_blocklist as $keyword) {

				// Skip empty keywords
				if(empty($keyword)) { continue; }

				// Search mode
				$search_mode = substr($keyword, 0, 1);

				switch($search_mode) {

					// Anywhere in the text
					case '*' :

						$keyword = substr($keyword, 1);

						if(stripos($text, $keyword) !== false) { return $keyword; }

						break;

					// Whole word match with padding and normalization
					default :

						$pattern = '/[\s\W]' . preg_quote($keyword, '/') . '[\s\W]/i';

						if(preg_match($pattern, $normalized)) { return $keyword; }
				}
			}

			return false;
		}

		// Process validation filter
		public function filter_validate($actions_old, $actions_new, $field = false, $field_id = false, $field_type_config = false, $section_repeatable_index = false) {

			// Check returned value, it should be an array
			if(
				($actions_new == $actions_old) ||
				!is_array($actions_new)
			) {
				return $actions_old;
			}

			// Legacy - Check if a single action array has been returned
			if(
				isset($actions_new['action']) ||
				isset($actions_new['field_id']) ||
				isset($actions_new['section_repeatable_index']) ||
				isset($actions_new['message'])
			) {

				// Wrap in array to make it match the new format
				$actions_diff = array($actions_new);

			} else {

				// Get only newly added actions from the filter actions
				$actions_diff = array_filter($actions_new, function ($actions_new_element) use ($actions_old) {

					return !in_array($actions_new_element, $actions_old);
				});
			}

			// Process newly added actions
			foreach($actions_diff as $index => $action) {

				// If string returned, create invalid feedback for that field
				if(is_string($action)) {

					if($field_id !== false) {

						// Field
						$actions_diff[$index] = array(

							'action' 					=> 'field_invalid_feedback',
							'field_id'					=> $field_id,
							'section_repeatable_index'	=> $section_repeatable_index,
							'message' 					=> $action
						);

					} else {

						// Form
						$actions_diff[$index] = array(

							'action' 					=> 'message',
							'message' 					=> $action
						);
					}

					continue;
				}

				// If false returned, create invalid feedback for that field
				if($action === false) {

					if($field_id !== false) {

						// Field
						$actions_diff[$index] = array(

							'action' 					=> 'field_invalid_feedback',
							'field_id' 					=> $field_id,
							'section_repeatable_index' 	=> $section_repeatable_index,
							'message' 					=> self::field_invalid_feedback($field, $field_type_config)
						);

					} else {

						// Form
						$actions_diff[$index] = array(

							'action' 					=> 'message',
							'message' 					=> __('An unknown error occurred.', 'ws-form')
						);
					}

					continue;
				}

				// Full action array returned
				if(is_array($action)) {

					// Add field ID if not found
					if(
						($field_id !== false) &&
						!isset($action['field_id'])
					) {
						$actions_diff[$index]['field_id'] = $field_id;
						$actions_diff[$index]['section_repeatable_index'] = $section_repeatable_index;
					}

					// Get action ID
					$action_id = isset($action['action']) ? $action['action'] : false;

					// Check by action type
					switch($action_id) {

						case 'field_invalid_feedback' :

							// Add message if not found
							if(
								($field_id !== false) &&
								($field_type_config !== false) &&
								!isset($action['message'])) {

								$actions_diff[$index]['message'] = self::field_invalid_feedback($field, $field_type_config);
							}

							break;

						case 'error' :
						case 'message' :

							if($action_id == 'error') {

								$actions_diff[$index]['action'] = 'message';
							}

							if(!isset($action['message'])) {

								if(
									($field_id !== false) &&
									($field_type_config !== false)
								) {

									$actions_diff[$index]['message'] = self::field_invalid_feedback($field, $field_type_config);

								} else {

									$actions_diff[$index]['message'] = __('An unknown error occurred', 'ws-form');
								}
							}

							break;
					}

					continue;
				}

				// Unknown validation type
				unset($actions_diff[$index]);
			}

			return (count($actions_diff) > 0) ? array_merge($actions_old, $actions_diff) : $actions_old;
		}

		// Process file fields
		public function process_file_fields($form_object) {

			// Get sections array
			$sections = WS_Form_Common::get_sections_from_form($form_object);

			// Get section_repeatable
			$section_repeatable = !empty($this->section_repeatable) ? WS_Form_Common::maybe_unserialize($this->section_repeatable) : array();

			// Is this a form submit?
			$form_submit = ($this->post_mode == 'submit');

			// Process each section
			foreach($sections as $section_id => $section) {

				if($section->repeatable) {

					$section_id_string = 'section_' . $section_id;

					// Get repeatable indexes for that section
					if(
						!isset($section_repeatable[$section_id_string]) ||
						!isset($section_repeatable[$section_id_string]['index'])
					) {

						parent::db_throw_error(__('Repeatable data error. Section ID not found in wsf_form_section_repeatable_index.', 'ws-form'));
					}

					$section_repeatable_indexes = $section_repeatable[$section_id_string]['index'];

					foreach($section_repeatable_indexes as $section_repeatable_index) {

						self::setup_from_post_section($section, $form_submit, true, false, array(), $section_id, $section_repeatable_index, $section_repeatable);
					}

				} else {

					self::setup_from_post_section($section, $form_submit, true, false);
				
				}
			}
		}

		// Get field invalid feedback
		public function field_invalid_feedback($field, $field_type_config) {

			// Get field invalid feedback
			$field_invalid_feedback = WS_Form_Common::get_object_meta_value($field, 'invalid_feedback', '');

			// Check invalid feedback
			if($field_invalid_feedback == '') {

				// Use form invalid feedback
				$field_invalid_feedback = WS_Form_Common::get_object_meta_value($this->form_object, 'invalid_feedback_mask', '');

				// Check invalid feedback
				if($field_invalid_feedback == '') {

					// Use default invalid feedback
					// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- All hooks prefixed with wsf_
					$field_invalid_feedback = apply_filters('wsf_field_invalid_feedback_text', __('This field is required.', 'ws-form'));
				}
			}

			// Get field label
			$invalid_feedback_label = $field->label;

			// Parse invalid_feedback_mask_placeholder
			$field_invalid_feedback = str_replace('#label_lowercase', strtolower($invalid_feedback_label), $field_invalid_feedback);
			$field_invalid_feedback = str_replace('#label', $invalid_feedback_label, $field_invalid_feedback);

			return $field_invalid_feedback;
		}

		// Meta value stringify
		public function field_value_stringify($field_object, $field_value, $field_submit_array, $delimiter_row) {

			$field_type = $field_object->type;

			if($field_submit_array) {

				if(!is_array($field_value)) { $field_value = array($field_value); }

				switch($field_type) {

					case 'file' :
					case 'mediacapture' :
					case 'signature' :

						$field_value = $field_value['name'];
						break;

					case 'googlemap' :

						if(
							is_array($field_value) &&
							isset($field_value['lat']) &&
							isset($field_value['lng'])
						) {

							$field_value = sprintf('%.7f,%.7f', $field_value['lat'], $field_value['lng']);

						} else {

							$field_value = '';
						}
						break;

					default :

						$field_value = implode($delimiter_row, $field_value);
				}

			} else {

				switch($field_type) {

					case 'datetime' :

						$field_value = WS_Form_Common::get_date_by_type($field_value, $field_object);;
						break;
				}
			}

			return $field_value;
		}

		// Read form object
		public function db_form_object_read($bypass_user_capability_check = false) {

			// Check form ID
			self::db_check_form_id();

			// Read form data
			$ws_form_form = new WS_Form_Form();
			$ws_form_form->id = $this->form_id;

			if($this->preview) {

				// Draft
				$form_object = $ws_form_form->db_read(true, true, false, true, $bypass_user_capability_check);

				// Form cannot be read
				if($form_object === false) { parent::db_throw_error(__('Unable to read form data. Still logged in?', 'ws-form')); }

			} else {

				// Published
				$form_object = $ws_form_form->db_read_published(true);

				// Form not yet published
				if($form_object === false) { parent::db_throw_error(__('No published form data.', 'ws-form')); }
			}

			// Filter
			// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- All hooks prefixed with wsf_
			$form_object = apply_filters('wsf_pre_render_' . $this->form_id, $form_object, $this->preview);
			// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- All hooks prefixed with wsf_
			$form_object = apply_filters('wsf_pre_render', $form_object, $this->preview);

			// Convert to object
			$this->form_object = $form_object;
		}

		// Process captcha
		public function db_captcha_process($field_id, $section_repeatable_index, $captcha_secret_key, $endpoint, $query_var) {

			// Check Captcha response
			if($captcha_secret_key == '') {

				parent::db_throw_error(__('Captcha secret key not set.', 'ws-form'));
			}

			// Get Captcha response
			$captcha_response = WS_Form_Common::get_query_var_nonce($query_var);
			if(empty($captcha_response)) {

				parent::db_throw_error(__('Invalid captcha response.', 'ws-form'));
			}

			// Body
			$body = array(

				'secret' => $captcha_secret_key,
				'response' => $captcha_response
			);

			// Remote IP (Only passed if Remote IP tracking is enabled)
			if(
				isset($this->meta) &&
				isset($this->meta['tracking_remote_ip'])
			) {

				$tracking_remote_ip = $this->meta['tracking_remote_ip'];

				if(!empty(WS_Form_Common::sanitize_ip_address($tracking_remote_ip))) {

					$body['remoteip'] = $tracking_remote_ip;
				}
			}

			// Build args
			$args = array(

				'user-agent'	=> WS_Form_Common::get_request_user_agent(),
				'timeout'		=> WS_Form_Common::get_request_timeout(),
				'sslverify'		=> WS_Form_Common::get_request_sslverify(),
				'redirection' 	=> 5,
				'httpversion' 	=> '1.0',
				'blocking' 		=> true,
				'headers' 		=> array(),
				'body' 			=> $body,
				'cookies' 		=> array()
			);

			// Get status of captcha from endpoint
			$response = wp_remote_post($endpoint, $args);

			// Check for errors
			if(is_wp_error($response)) {

				$error_message = $response->get_error_message();

				/* translators: %s: Error message */
				parent::db_throw_error(sprintf(__('Captcha verification failed (%s).', 'ws-form'), $error_message));

			} else {

				$response_body = wp_remote_retrieve_body($response); 
				if($response_body == '') {

					parent::db_throw_error(__('Captcha verification response empty.', 'ws-form'));
				}

				$response_object = json_decode($response_body);
				if(is_null($response_object)) {

					parent::db_throw_error(__('Captcha verification response error.', 'ws-form'));
				}

				$captcha_success = $response_object->success;

				if($captcha_success) {

					// Store spam level
					$this->spam_level = isset($response_object->score) ? ((1 - floatval($response_object->score)) * WS_FORM_SPAM_LEVEL_MAX) : $this->spam_level;

					return true;

				} else {

					if(
						isset($response_object->{'error-codes'}) &&
						is_array($response_object->{'error-codes'})
					) {

						foreach($response_object->{'error-codes'} as $error_code) {

							switch($error_code) {

								case 'missing-input-secret' :

									$error_message = __('Captcha Error: The secret parameter was not passed.', 'ws-form');
									break;

								case 'invalid-input-secret' :

									$error_message = __('Captcha Error: The secret parameter was invalid or did not exist.', 'ws-form');
									break;

								case 'missing-input-response' :

									$error_message = __('Captcha Error: The response parameter was not passed.', 'ws-form');
									break;

								case 'invalid-input-response' :

									$error_message = __('Captcha Error: The response parameter is invalid or has expired.', 'ws-form');
									break;

								case 'bad-request' :

									$error_message = __('Captcha Error: The request was rejected because it was malformed.', 'ws-form');
									break;

								case 'timeout-or-duplicate' :

									$error_message = __('Captcha Error: The response parameter has already been validated before.', 'ws-form');
									break;

								case 'internal-error' :

									$error_message = __('Captcha Error: An internal error happened while validating the response. The request can be retried.', 'ws-form');
									break;

								default :

									/* translators: %s: Error code */
									$error_message = sprintf(__('Captcha Error: %s.', 'ws-form'), $error_code);
							}

							// Throw error
							parent::db_throw_error($error_message);
						}
					}

					// Handle error
					parent::db_throw_error(__('Captcha invalid.', 'ws-form'));
				}
			}

			return true;
		}

		// Clear hidden meta values
		public function clear_hidden_meta_values($submit_object = false, $bypass_user_capability_check = true) {

			if($submit_object === false) {

				$submit_object = $this;
			}

			if(!isset($submit_object->meta)) { return $submit_object; }
			if(!isset($submit_object->meta['wsf_meta_key_hidden'])) { return $submit_object; }

			// Get section repeatable data (Unserialize if necesary)
			$section_repeatable_serialized = false;
			if(isset($submit_object->section_repeatable)) {

				$section_repeatable_serialized = is_serialized($submit_object->section_repeatable);

				$section_repeatable_array = $section_repeatable_serialized ? WS_Form_Common::maybe_unserialize($submit_object->section_repeatable) : $submit_object->section_repeatable;

				if(!is_array($section_repeatable_array)) { $section_repeatable_array = array(); }

			} else {

				$section_repeatable_array = array();
			}
			$section_repeatable_original_array = $section_repeatable_array;
			$section_repeatable_edited = false;

			// Get hidden field names
			$meta_key_hidden_array = $submit_object->meta['wsf_meta_key_hidden'];

			// Clear each hidden array
			$field_ids_hidden = array();
			$field_ids_need_new_fallback = array();

			foreach($meta_key_hidden_array as $meta_key_hidden) {

				if(
					!isset($submit_object->meta[$meta_key_hidden]) ||
					!isset($submit_object->meta[$meta_key_hidden]['id'])
				) {
					continue;
				}

				// Get field ID
				$field_id = absint($submit_object->meta[$meta_key_hidden]['id']);

				// Get section ID (Only set on repeatable sections)
				$section_id = isset($submit_object->meta[$meta_key_hidden]['section_id']) ? absint($submit_object->meta[$meta_key_hidden]['section_id']) : 0;
				if($section_id > 0) {

					if(!isset($field_ids_hidden[$section_id])) { $field_ids_hidden[$section_id] = array(); }

					// Add to fields touched
					if(!isset($field_ids_hidden[$section_id][$field_id])) { $field_ids_hidden[$section_id][$field_id] = 0; }
					$field_ids_hidden[$section_id][$field_id]++;
				}

				// Unset field
				unset($submit_object->meta[$meta_key_hidden]);

				// Unset fallback field
				unset($submit_object->meta[WS_FORM_FIELD_PREFIX . $field_id]);

				$field_ids_need_new_fallback[] = $field_id;
			}

			$field_ids_need_new_fallback = array_unique($field_ids_need_new_fallback);

			if(count($field_ids_need_new_fallback) > 0) {

				// Run through each section and clean section repeatable array
				foreach($field_ids_hidden as $section_id => $fields) {

					// Get section name
					$section_name = sprintf('section_%u', $section_id);

					// Check this exists in the index
					if(!isset($section_repeatable_array[$section_name])) { continue; }

					// Run through each index
					foreach($section_repeatable_array[$section_name]['index'] as $section_repeatable_index => $section_repeatable_id) {

						// Find out how many fields remain for this section
						$section_row_fields_found = false;
						foreach($submit_object->meta as $meta) {

							if(
								!isset($meta['section_id']) ||
								!isset($meta['repeatable_index'])
							) {
								continue;
							}

							// Get section ID (Only set on repeatable sections)
							$meta_section_id = absint($meta['section_id']);
							$meta_repeatable_index = absint($meta['repeatable_index']);
							if(
								($meta_section_id === $section_id) &&
								($meta_repeatable_index === $section_repeatable_id)
							) {

								$section_row_fields_found = true;
								break;
							}
						}

						// If no fields found in this row, then remove it from the section repeatable array
						if(!$section_row_fields_found) {

							// Remove this row from the index
							foreach($section_repeatable_array[$section_name]['index'] as $section_repeatable_index_delete => $section_repeatable_id_delete) {

								if($section_repeatable_id_delete === $section_repeatable_id) {

									unset($section_repeatable_array[$section_name]['index'][$section_repeatable_index_delete]);
									$section_repeatable_array[$section_name]['index'] = array_values($section_repeatable_array[$section_name]['index']);
									$section_repeatable_edited = true;
								}
							}
						}
					}
				}

				// Rebuild meta data
				$meta_array = array();
				foreach($submit_object->meta as $meta_key => $meta) {

					if(
						!isset($meta['value']) ||
						!isset($meta['section_id']) ||
						!isset($meta['id']) ||
						!isset($meta['repeatable_index'])
					) {

						continue;
					}

					// Strip db_ignore
					if(isset($meta['db_ignore']) && $meta['db_ignore']) { continue; }

					// Build meta data
					$meta_array[] = array(

						// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
						'meta_key' => $meta_key,
						// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
						'meta_value' => $meta['value'],
						'section_id' => $meta['section_id'],
						'field_id' => $meta['id'],
						'repeatable_index' => $meta['repeatable_index']
					);
				}

				// Get new fallback values
				$meta_new = self::db_get_submit_meta($this, $meta_array, $bypass_user_capability_check);

				// Run through field that needs a new fallback
				foreach($field_ids_need_new_fallback as $field_id) {

					// Field name
					$field_name = WS_FORM_FIELD_PREFIX . $field_id;
					if(isset($meta_new[$field_name])) {

						// We don't store the fallback data to the database, it is just made available to any actions that need it
						$meta_new[$field_name]['db_ignore'] = true;

						// Set repeatable index to false
						$meta_new[$field_name]['repeatable_index'] = false;

						// Replace						
						$this->meta[$field_name] = $submit_object->meta[$field_name] = $meta_new[$field_name];
					}
				}

				// Rebuild section_repeatable
				if($section_repeatable_edited) {

					$this->section_repeatable = $submit_object->section_repeatable = ($section_repeatable_serialized ? serialize($section_repeatable_array) : $section_repeatable_array);
				}
			}

			return $submit_object;
		}

		// Handle server side error - Invalid feedback
		public function db_throw_error_field_invalid_feedback($field_id, $section_repeatable_index, $message) {

			// Only process first error for field
			if(isset($this->error_validation_actions[$field_id])) { return; }

			$this->error_validation_actions[$field_id] = array(

				'action' 					=> 'field_invalid_feedback',
				'field_id' 					=> $field_id,
				'section_repeatable_index' 	=> $section_repeatable_index,
				'message' 					=> $message
			);
		}

		// Find submit meta value by type (first_name, last_name, email) 
		public function find_field_value($field_type) {

			$fields = \WS_Form_Common::get_fields_from_form( $this->form_object, true );

			switch($field_type) {			

				case 'first_name':
					$possible_matches = [ 'first name', 'firstname', 'given name', 'forename' ];
					break;

				case 'last_name':
					$possible_matches = [ 'last name', 'lastname', 'surname', 'family name' ];
					break;

				case 'email':
					$possible_matches = [];
					break;

				default:
					return false;
			}

			$is_full_name = false;

			// Look for specific first, last or email type
			$field_id = self::find_field_value_process($fields, $field_type, $possible_matches);

			// If not found, look for possible full name matches
			if(
				($field_id === false) &&
				in_array($field_type, array('first_name', 'last_name'))
			) {
				$possible_matches = [ 'full name', 'your name', 'name' ];

				$field_id = self::find_field_value_process($fields, $field_type, $possible_matches);

				$is_full_name = true;
			}

			if($field_id === false) { return false; }

			// Build meta key and get value
			$meta_key = sprintf( 'field_%u', $field_id );
			$value    = trim( \WS_Form_Action::get_submit_value( $this, $meta_key ) );

			// Handle full name field using existing parser
			if ( $is_full_name ) {

				$components = \WS_Form_Common::get_full_name_components( $value );

				switch ( $field_type ) {
					case 'first_name':
						return !empty( $components['name_first'] ) ? $components['name_first'] : false;

					case 'last_name':
						return !empty( $components['name_last'] ) ? $components['name_last'] : false;

					default:
						return false;
				}
			}

			return $value ?: false;
		}

		public function find_field_value_process($fields, $field_type, $possible_matches) {

			$field_id = false;

			foreach ( $fields as $field ) {

				if ( empty( $field->type ) ) { continue; }

				// Handle email field
				if ( $field_type === 'email' ) {

					if($field->type === 'email') {

						$field_id = $field->id;
						break;
					}

					continue;
				}

				// Handle text-based fields for name detection
				if ( $field->type === 'text' ) {

					$label = strtolower( trim( $field->label ?? '' ) );

					foreach ( $possible_matches as $match ) {
						if ( preg_match( '/\b' . preg_quote( $match, '/' ) . '\b/', $label ) ) {
							$field_id = $field->id;
							break 2;
						}
					}
				}
			}

			return $field_id;
		}

		// Remove protected meta data
		public function db_remove_meta_protected() {

			$this->meta_protected = array();
		}

		// Compact
		public function db_compact() {

			// Remove form_object
			if(isset($this->form_object)) { unset($this->form_object); }
			if(isset($this->field_types)) { unset($this->field_types); }
		}

		// Create hash
		public function db_create_hash() {

			if($this->hash == '') { $this->hash = esc_sql(wp_hash($this->id . '_' . $this->form_id . '_' . time() . '_' . wp_rand())); }

			// Check hash
			if(!WS_Form_Common::check_submit_hash($this->hash)) {

				self::return_forbidden();
			}

			return $this->hash;
		}

		// Create token
		public function db_create_token() {

			if(!WS_Form_Common::check_submit_hash($this->hash)) {

				self::return_forbidden();
			}

			if($this->token == '') { $this->token = esc_sql(wp_hash($this->id . '_' . $this->form_id . '_' . $this->token . '_' . time() . '_' . wp_rand())); }

			// Check hash
			if(!WS_Form_Common::check_submit_hash($this->token)) {

				self::return_forbidden();
			}

			return $this->token;
		}

		// Check form id
		public function db_check_form_id() {

			if(absint($this->form_id) === 0) { parent::db_throw_error(__('Invalid form ID (WS_Form_Submit | db_check_form_id)', 'ws-form')); }
			return true;
		}

		// Check id
		public function db_check_id() {

			if(absint($this->id) === 0) { parent::db_throw_error(__('Invalid submit ID (WS_Form_Submit | db_check_id)', 'ws-form')); }
			return true;
		}

		// Is valid
		public function is_valid($submit_object) {

			return (
				is_object($submit_object) &&
				property_exists($submit_object, 'id')
			);
		}
	}