<?php

use WPForms\Pro\Forms\Fields\FileUpload\Chunk;

/**
 * File upload field.
 *
 * @since 1.0.0
 */
class WPForms_Field_File_Upload extends WPForms_Field {

	/**
	 * Dropzone plugin version.
	 *
	 * @since 1.5.6
	 *
	 * @var string
	 */
	const DROPZONE_VERSION = '5.7.2';

	/**
	 * Classic (old) style of file uploader field.
	 *
	 * @since 1.5.6
	 *
	 * @var string
	 */
	const STYLE_CLASSIC = 'classic';

	/**
	 * Modern style of file uploader field.
	 *
	 * @since 1.5.6
	 *
	 * @var string
	 */
	const STYLE_MODERN = 'modern';

	/**
	 * Replaceable (either in PHP or JS) template for a maximum file number.
	 *
	 * @since 1.5.8
	 *
	 * @var string
	 */
	const TEMPLATE_MAXFILENUM = '{maxFileNumber}';

	/**
	 * File extensions that are now allowed.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	private $denylist = array( 'ade', 'adp', 'app', 'asp', 'bas', 'bat', 'cer', 'cgi', 'chm', 'cmd', 'com', 'cpl', 'crt', 'csh', 'csr', 'dll', 'drv', 'exe', 'fxp', 'flv', 'hlp', 'hta', 'htaccess', 'htm', 'html', 'htpasswd', 'inf', 'ins', 'isp', 'jar', 'js', 'jse', 'jsp', 'ksh', 'lnk', 'mdb', 'mde', 'mdt', 'mdw', 'msc', 'msi', 'msp', 'mst', 'ops', 'pcd', 'php', 'pif', 'pl', 'prg', 'ps1', 'ps2', 'py', 'rb', 'reg', 'scr', 'sct', 'sh', 'shb', 'shs', 'sys', 'swf', 'tmp', 'torrent', 'url', 'vb', 'vbe', 'vbs', 'vbscript', 'wsc', 'wsf', 'wsf', 'wsh', 'dfxp', 'onetmp' );

	/**
	 * Primary class constructor.
	 *
	 * @since 1.0.0
	 */
	public function init() {

		// Define field type information.
		$this->name  = esc_html__( 'File Upload', 'wpforms' );
		$this->type  = 'file-upload';
		$this->icon  = 'fa-upload';
		$this->order = 90;
		$this->group = 'fancy';

		// Form frontend javascript.
		add_action( 'wpforms_frontend_js', array( $this, 'frontend_js' ) );

		// Form frontend CSS.
		add_action( 'wpforms_frontend_css', array( $this, 'frontend_css' ) );

		// Field styles for Gutenberg.
		add_action( 'enqueue_block_editor_assets', array( $this, 'gutenberg_enqueues' ) );

		// Define additional field properties.
		add_filter( 'wpforms_field_properties_file-upload', array( $this, 'field_properties' ), 5, 3 );

		// Customize value format for HTML emails.
		add_filter( 'wpforms_html_field_value', array( $this, 'html_email_value' ), 10, 4 );

		// Add builder strings.
		add_filter( 'wpforms_builder_strings', array( $this, 'add_builder_strings' ), 10, 2 );

		// Maybe format/upload file depending on the conditional visibility state.
		add_action( 'wpforms_process_format_after', array( $this, 'format_conditional' ), 6, 1 );

		// Upload file ajax route.
		add_action( 'wp_ajax_wpforms_file_upload_speed_test', 'wp_send_json_success' );
		add_action( 'wp_ajax_nopriv_wpforms_file_upload_speed_test', 'wp_send_json_success' );

		// TODO: perhaps remove, chunks uploading replaces this.
		add_action( 'wp_ajax_wpforms_upload_file', array( $this, 'ajax_modern_upload' ) );
		add_action( 'wp_ajax_nopriv_wpforms_upload_file', array( $this, 'ajax_modern_upload' ) );

		// Ajax handlers for newest uploads (With chunks and parallel support).
		add_action( 'wp_ajax_wpforms_upload_chunk_init', array( $this, 'ajax_chunk_upload_init' ) );
		add_action( 'wp_ajax_nopriv_wpforms_upload_chunk_init', array( $this, 'ajax_chunk_upload_init' ) );

		add_action( 'wp_ajax_wpforms_upload_chunk', array( $this, 'ajax_chunk_upload' ) );
		add_action( 'wp_ajax_nopriv_wpforms_upload_chunk', array( $this, 'ajax_chunk_upload' ) );

		add_action( 'wp_ajax_wpforms_file_chunks_uploaded', array( $this, 'ajax_chunk_upload_finalize' ) );
		add_action( 'wp_ajax_nopriv_wpforms_file_chunks_uploaded', array( $this, 'ajax_chunk_upload_finalize' ) );

		// Remove file ajax route.
		add_action( 'wp_ajax_wpforms_remove_file', array( $this, 'ajax_modern_remove' ) );
		add_action( 'wp_ajax_nopriv_wpforms_remove_file', array( $this, 'ajax_modern_remove' ) );

		// phpcs:ignore WordPress.Security.NonceVerification
		if ( ! empty( $_POST['slow'] ) && 'true' === $_POST['slow'] && ! empty( $this->ajax_validate_form_field_modern() ) ) {
			add_action( 'wpforms_file_upload_chunk_parallel', '__return_false' );
			add_action( 'wpforms_file_upload_chunk_size', array( $this, 'get_slow_connection_chunk_size' ) );
		}

		add_filter( 'robots_txt', array( $this, 'disallow_upload_dir_indexing' ), -42 );
	}

	/**
	 * Enqueue frontend field js.
	 *
	 * @since 1.5.6
	 *
	 * @param array $forms Forms on the current page.
	 */
	public function frontend_js( $forms ) {

		$is_file_modern_style = false;

		foreach ( $forms as $form ) {
			if ( $this->is_field_style( $form, self::STYLE_MODERN ) ) {
				$is_file_modern_style = true;

				break;
			}
		}

		if (
			$is_file_modern_style ||
			wpforms()->frontend->assets_global()
		) {

			$min = wpforms_get_min_suffix();

			wp_enqueue_script(
				'wpforms-dropzone',
				WPFORMS_PLUGIN_URL . "pro/assets/js/vendor/dropzone{$min}.js",
				array( 'jquery' ),
				self::DROPZONE_VERSION,
				true
			);

			wp_enqueue_script(
				'wpforms-file-upload',
				WPFORMS_PLUGIN_URL . "pro/assets/js/wpforms-file-upload{$min}.js",
				array( 'wpforms', 'wp-util', 'wpforms-dropzone' ),
				WPFORMS_VERSION,
				true
			);

			wp_localize_script(
				'wpforms-dropzone',
				'wpforms_file_upload',
				array(
					'url'             => admin_url( 'admin-ajax.php' ),
					'errors'          => array(
						'file_not_uploaded' => esc_html__( 'This file was not uploaded.', 'wpforms' ),
						'file_limit'        => esc_html__( 'File limit has been reached ({fileLimit}).', 'wpforms' ),
						'file_extension'    => wpforms_setting( 'validation-fileextension', esc_html__( 'File type is not allowed.', 'wpforms' ) ),
						'file_size'         => wpforms_setting( 'validation-filesize', esc_html__( 'File exceeds the max size allowed.', 'wpforms' ) ),
						'post_max_size'     => sprintf( /* translators: %s - max allowed file size by a server. */
							esc_html__( 'File exceeds the upload limit allowed (%s).', 'wpforms' ),
							wpforms_max_upload()
						),
					),
					'loading_message' => esc_html__( 'File upload is in progress. Please submit the form once uploading is completed.', 'wpforms' ),
				)
			);
		}
	}

	/**
	 * Enqueue frontend field CSS.
	 *
	 * @since 1.5.6
	 *
	 * @param array $forms Forms on the current page.
	 */
	public function frontend_css( $forms ) {

		$is_file_modern_style = false;

		foreach ( $forms as $form ) {
			if ( $this->is_field_style( $form, self::STYLE_MODERN ) ) {
				$is_file_modern_style = true;

				break;
			}
		}

		if (
			$is_file_modern_style ||
			wpforms()->frontend->assets_global()
		) {

			$min = wpforms_get_min_suffix();

			wp_enqueue_style(
				'wpforms-dropzone',
				WPFORMS_PLUGIN_URL . "pro/assets/css/dropzone{$min}.css",
				array(),
				self::DROPZONE_VERSION
			);
		}
	}

	/**
	 * Whether provided form has a file field with a specified style.
	 *
	 * @since 1.5.6
	 *
	 * @param array  $form  Form data.
	 * @param string $style Desired field style.
	 *
	 * @return bool
	 */
	protected function is_field_style( $form, $style ) {

		$is_field_style = false;

		if ( empty( $form['fields'] ) ) {
			return $is_field_style;
		}

		foreach ( (array) $form['fields'] as $field ) {

			if (
				! empty( $field['type'] ) &&
				$field['type'] === $this->type &&
				! empty( $field['style'] ) &&
				$field['style'] === sanitize_key( $style )
			) {
				$is_field_style = true;

				break;
			}
		}

		return $is_field_style;
	}

	/**
	 * Load enqueues for the Gutenberg editor.
	 *
	 * @since 1.5.6
	 */
	public function gutenberg_enqueues() {

		$min = wpforms_get_min_suffix();

		wp_enqueue_style(
			'wpforms-dropzone',
			WPFORMS_PLUGIN_URL . "pro/assets/css/dropzone{$min}.css",
			array(),
			self::DROPZONE_VERSION
		);
	}

	/**
	 * Define additional field properties.
	 *
	 * @since 1.3.7
	 *
	 * @param array $properties Field properties.
	 * @param array $field      Field data and settings.
	 * @param array $form_data  Form data and settings.
	 *
	 * @return array
	 */
	public function field_properties( $properties, $field, $form_data ) {

		$this->form_data  = (array) $form_data;
		$this->form_id    = absint( $this->form_data['id'] );
		$this->field_id   = absint( $field['id'] );
		$this->field_data = $this->form_data['fields'][ $this->field_id ];

		// Input Primary: adjust name.
		$properties['inputs']['primary']['attr']['name'] = "wpforms_{$this->form_id}_{$this->field_id}";

		// Input Primary: filter files in classic uploader style in files selection window.
		if ( empty( $this->field_data['style'] ) || self::STYLE_CLASSIC === $this->field_data['style'] ) {
			$properties['inputs']['primary']['attr']['accept'] = rtrim( '.' . implode( ',.', $this->get_extensions() ), ',.' );
		}

		// Input Primary: allowed file extensions.
		$properties['inputs']['primary']['data']['rule-extension'] = implode( ',', $this->get_extensions() );

		// Input Primary: max file size.
		$properties['inputs']['primary']['data']['rule-maxsize'] = $this->max_file_size();

		return $properties;
	}

	/**
	 * Whether current field can be populated dynamically.
	 *
	 * @since 1.5.0
	 *
	 * @param array $properties Field properties.
	 * @param array $field      Current field specific data.
	 *
	 * @return bool
	 */
	public function is_dynamic_population_allowed( $properties, $field ) {

		// We need to disable an ability to steal files from user computer.
		return false;
	}

	/**
	 * Whether current field can be populated dynamically.
	 *
	 * @since 1.5.0
	 *
	 * @param array $properties Field properties.
	 * @param array $field      Current field specific data.
	 *
	 * @return bool
	 */
	public function is_fallback_population_allowed( $properties, $field ) {

		// We need to disable an ability to steal files from user computer.
		return false;
	}

	/**
	 * Customize format for HTML email notifications.
	 *
	 * @since 1.1.3
	 * @since 1.5.6 Added different link generation for classic and modern uploader.
	 *
	 * @param string $val       Field value.
	 * @param array  $field     Field settings.
	 * @param array  $form_data Form data and settings.
	 * @param string $context   Value display context.
	 *
	 * @return string
	 */
	public function html_email_value( $val, $field, $form_data = array(), $context = '' ) {

		if ( empty( $field['value'] ) || $field['type'] !== $this->type ) {
			return $val;
		}

		// Process modern uploader.
		if ( ! empty( $field['value_raw'] ) ) {
			return wpforms_chain( $field['value_raw'] )
				->map(
					static function ( $file ) {

						if ( empty( $file['value'] ) || empty( $file['file_original'] ) ) {
							return '';
						}

						return sprintf(
							'<a href="%s" rel="noopener noreferrer" target="_blank">%s</a>',
							esc_url( $file['value'] ),
							esc_html( $file['file_original'] )
						);
					}
				)
				->array_filter()
				->implode( '<br>' )
				->value();
		}

		// Process classic uploader.
		return sprintf(
			'<a href="%s" rel="noopener" target="_blank">%s</a>',
			esc_url( $field['value'] ),
			esc_html( $field['file_original'] )
		);
	}

	/**
	 * File Upload field specific strings.
	 *
	 * @since 1.5.8
	 *
	 * @return array Field specific strings.
	 */
	public function get_strings() {

		return array(
			'preview_title_single' => esc_html__( 'Click or drag a file to this area to upload.', 'wpforms' ),
			'preview_title_plural' => esc_html__( 'Click or drag files to this area to upload.', 'wpforms' ),
			'preview_hint'         => sprintf( /* translators: % - max number of files as a template string (not a number), replaced by a number later. */
				esc_html__( 'You can upload up to %s files.', 'wpforms' ),
				self::TEMPLATE_MAXFILENUM
			),
		);
	}

	/**
	 * Add Builder strings that are passed to JS.
	 *
	 * @since 1.5.8
	 *
	 * @param array $strings Form Builder strings.
	 * @param array $form    Form Data.
	 *
	 * @return array Form Builder strings.
	 */
	public function add_builder_strings( $strings, $form ) {

		$strings['file_upload'] = $this->get_strings();

		return $strings;
	}

	/**
	 * Field options panel inside the builder.
	 *
	 * @since 1.0.0
	 * @since 1.5.6 Added modern style uploader options.
	 *
	 * @param array $field Field data and settings.
	 */
	public function field_options( $field ) {

		$style = ! empty( $field['style'] ) ? $field['style'] : self::STYLE_MODERN;

		/*
		 * Basic field options.
		 */

		// Options open markup.
		$this->field_option( 'basic-options', $field, array( 'markup' => 'open' ) );

		// Label.
		$this->field_option( 'label', $field );

		// Description.
		$this->field_option( 'description', $field );

		// Allowed extensions.
		$lbl = $this->field_element(
			'label',
			$field,
			array(
				'slug'          => 'extensions',
				'value'         => esc_html__( 'Allowed File Extensions', 'wpforms' ),
				'tooltip'       => esc_html__( 'Enter the extensions you would like to allow, comma separated.', 'wpforms' ),
				'after_tooltip' => '<a href="https://wpforms.com/docs/a-complete-guide-to-the-file-upload-field/#file-types" class="after-label-description" target="_blank" rel="noopener noreferrer">' . esc_html__( 'See More Details', 'wpforms' ) . '</a>',
			),
			false
		);
		$fld = $this->field_element(
			'text',
			$field,
			array(
				'slug'  => 'extensions',
				'value' => ! empty( $field['extensions'] ) ? $field['extensions'] : '',
			),
			false
		);
		$this->field_element(
			'row',
			$field,
			array(
				'slug'    => 'extensions',
				'content' => $lbl . $fld,
			)
		);

		// Max file size.
		$lbl = $this->field_element(
			'label',
			$field,
			array(
				'slug'    => 'max_size',
				'value'   => esc_html__( 'Max File Size', 'wpforms' ),
				/* translators: %s - max upload size. */
				'tooltip' => sprintf( esc_html__( 'Enter the max size of each file, in megabytes, to allow. If left blank, the value defaults to the maximum size the server allows which is %s.', 'wpforms' ), wpforms_max_upload() ),
			),
			false
		);
		$fld = $this->field_element(
			'text',
			$field,
			array(
				'slug'  => 'max_size',
				'type'  => 'number',
				'attrs' => array(
					'min'     => 1,
					'max'     => 512,
					'step'    => 1,
					'pattern' => '[0-9]',
				),
				'value' => ! empty( $field['max_size'] ) ? abs( $field['max_size'] ) : '',
			),
			false
		);
		$this->field_element(
			'row',
			$field,
			array(
				'slug'    => 'max_size',
				'content' => $lbl . $fld,
			)
		);

		// Max file number.
		$lbl = $this->field_element(
			'label',
			$field,
			array(
				'slug'    => 'max_file_number',
				'value'   => esc_html__( 'Max File Number', 'wpforms' ),
				'tooltip' => esc_html__( 'Enter the max file number, to allow. If left blank, the value defaults to 1.', 'wpforms' ),
			),
			false
		);
		$fld = $this->field_element(
			'text',
			$field,
			array(
				'slug'  => 'max_file_number',
				'type'  => 'number',
				'attrs' => array(
					'min'     => 1,
					'max'     => 100,
					'step'    => 1,
					'pattern' => '[0-9]',
				),
				'value' => ! empty( $field['max_file_number'] ) ? absint( $field['max_file_number'] ) : 1,
			),
			false
		);
		$this->field_element( 'row', $field, array(
			'slug'    => 'max_file_number',
			'content' => $lbl . $fld,
			'class'   => self::STYLE_CLASSIC === $style ? 'wpforms-row-hide' : '',
		) );

		// Required toggle.
		$this->field_option( 'required', $field );

		// Options close markup.
		$this->field_option( 'basic-options', $field, array( 'markup' => 'close' ) );

		/*
		 * Advanced field options.
		 */

		// Options open markup.
		$this->field_option( 'advanced-options', $field, array( 'markup' => 'open' ) );

		// Style.
		$lbl = $this->field_element(
			'label',
			$field,
			array(
				'slug'    => 'style',
				'value'   => esc_html__( 'Style', 'wpforms' ),
				'tooltip' => esc_html__( 'Modern Style supports multiple file uploads, displays a drag-and-drop upload box, and uses AJAX. Classic Style supports single file upload and displays a traditional upload button.', 'wpforms' ),
			),
			false
		);
		$fld = $this->field_element(
			'select',
			$field,
			array(
				'slug'    => 'style',
				'value'   => $style,
				'options' => array(
					self::STYLE_MODERN  => esc_html__( 'Modern', 'wpforms' ),
					self::STYLE_CLASSIC => esc_html__( 'Classic', 'wpforms' ),
				),
			),
			false
		);
		$this->field_element( 'row', $field, array(
			'slug'    => 'style',
			'content' => $lbl . $fld,
		) );

		// Hide Label.
		$this->field_option( 'label_hide', $field );

		// Media Library toggle.
		$fld  = $this->field_element(
			'checkbox',
			$field,
			array(
				'slug'    => 'media_library',
				'value'   => ! empty( $field['media_library'] ) ? 1 : '',
				'desc'    => esc_html__( 'Store file in WordPress Media Library', 'wpforms' ),
				'tooltip' => esc_html__( 'Check this option to store the final uploaded file in the WordPress Media Library', 'wpforms' ),
			),
			false
		);
		$this->field_element( 'row', $field, array(
			'slug'    => 'media_library',
			'content' => $fld,
		) );

		// Custom CSS classes.
		$this->field_option( 'css', $field );

		// Options close markup.
		$this->field_option( 'advanced-options', $field, array(
			'markup' => 'close',
		) );
	}

	/**
	 * Field preview panel inside the builder.
	 *
	 * @since 1.0.0
	 * @since 1.5.6 Added modern style uploader logic.
	 *
	 * @param array $field Field data.
	 */
	public function field_preview( $field ) {

		// Label.
		$this->field_preview_option( 'label', $field );

		$modern_classes  = array( 'wpforms-file-upload-builder-modern' );
		$classic_classes = array( 'wpforms-file-upload-builder-classic' );
		if ( empty( $field['style'] ) || self::STYLE_CLASSIC !== $field['style'] ) {
			$classic_classes[] = 'wpforms-hide';
		} else {
			$modern_classes[] = 'wpforms-hide';
		}

		$strings         = $this->get_strings();
		$max_file_number = ! empty( $field['max_file_number'] ) ? max( 1, absint( $field['max_file_number'] ) ) : 1;

		// Primary input.
		echo wpforms_render(
			'fields/file-upload-backend',
			array(
				'max_file_number' => $max_file_number,
				'preview_hint'    => str_replace( self::TEMPLATE_MAXFILENUM, $max_file_number, $strings['preview_hint'] ),
				'modern_classes'  => implode( ' ', $modern_classes ),
				'classic_classes' => implode( ' ', $classic_classes ),
			),
			true
		);

		// Description.
		$this->field_preview_option( 'description', $field );
	}

	/**
	 * Field display on the form front-end.
	 *
	 * @since 1.0.0
	 * @since 1.5.6 Added modern style uploader logic.
	 *
	 * @param array $field      Field data and settings.
	 * @param array $deprecated Deprecated field attributes. Use field properties.
	 * @param array $form_data  Form data and settings.
	 */
	public function field_display( $field, $deprecated, $form_data ) {

		// Define data.
		$primary = $field['properties']['inputs']['primary'];

		// Modern style.
		if ( ! empty( $field['style'] ) && self::STYLE_MODERN === $field['style'] ) {

			$strings         = $this->get_strings();
			$max_file_number = ! empty( $field['max_file_number'] ) ? max( 1, absint( $field['max_file_number'] ) ) : 1;

			// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
			echo wpforms_render(
				'fields/file-upload-frontend',
				array(
					'field_id'        => (int) $field['id'],
					'form_id'         => (int) $form_data['id'],
					'url'             => admin_url( 'admin-ajax.php' ),
					'input_name'      => $this->get_input_name(),
					'required'        => $primary['required'],
					'extensions'      => $primary['data']['rule-extension'],
					'max_size'        => abs( $primary['data']['rule-maxsize'] ),
					'chunk_size'      => $this->get_chunk_size(),
					'max_file_number' => $max_file_number,
					'preview_hint'    => str_replace( self::TEMPLATE_MAXFILENUM, $max_file_number, $strings['preview_hint'] ),
					'post_max_size'   => wp_max_upload_size(),
				),
				true
			);
		} else {
			// Classic style.
			printf(
				'<input type="file" %s %s>',
				wpforms_html_attributes( $primary['id'], $primary['class'], $primary['data'], $primary['attr'] ),
				$primary['required'] // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
			);
		}
	}

	/**
	 * Input name.
	 *
	 * The input name is name in which the data is expected to be sent in from the client.
	 *
	 * @since 1.6.2
	 *
	 * @return string
	 */
	public function get_input_name() {

		return sprintf( 'wpforms_%d_%d', $this->form_id, $this->field_id );
	}

	/**
	 * Maximum size for a chunk in file uploads.
	 *
	 * @since 1.6.2
	 *
	 * @return int
	 */
	public function get_chunk_size() {

		return min(
			apply_filters( 'wpforms_file_upload_chunk_size', 2 * 1024 * 1024 ),
			wp_max_upload_size(),
			$this->max_file_size()
		);
	}

	/**
	 * Maximum chunk for slow connections.
	 *
	 * @since 1.6.2
	 *
	 * @return int Chunk size expected for slow connections.
	 */
	public function get_slow_connection_chunk_size() {

		return min(
			512 * 1024,
			wp_max_upload_size(),
			$this->max_file_size()
		);
	}

	/**
	 * Validate field for various errors on form submit.
	 *
	 * @since 1.0.0
	 * @since 1.5.6 Added modern style uploader logic.
	 *
	 * @param int   $field_id     Field ID.
	 * @param array $field_submit Submitted field value.
	 * @param array $form_data    Form data and settings.
	 */
	public function validate( $field_id, $field_submit, $form_data ) {

		$this->form_data  = (array) $form_data;
		$this->form_id    = absint( $this->form_data['id'] );
		$this->field_id   = absint( $field_id );
		$this->field_data = $this->form_data['fields'][ $this->field_id ];

		$input_name = $this->get_input_name();
		$style      = ! empty( $this->field_data['style'] ) ? $this->field_data['style'] : self::STYLE_CLASSIC;

		// Add modern validate.
		if ( self::STYLE_CLASSIC === $style ) {
			$this->validate_classic( $input_name );
		} else {
			$this->validate_modern( $input_name );
		}
	}

	/**
	 * Validate classic file uploader field data.
	 *
	 * @since 1.5.6
	 *
	 * @param string $input_name Input name inside the form on front-end.
	 */
	protected function validate_classic( $input_name ) {

		if ( empty( $_FILES[ $input_name ] ) ) {
			return;
		}

		/*
		 * If nothing is uploaded and it is not required, don't process.
		 */
		if ( $_FILES[ $input_name ]['error'] === 4 && ! $this->is_required() ) {
			return;
		}

		/*
		 * Basic file upload validation.
		 */
		$validated_basic = $this->validate_basic( (int) $_FILES[ $input_name ]['error'] );
		if ( ! empty( $validated_basic ) ) {
			wpforms()->process->errors[ $this->form_id ][ $this->field_id ] = $validated_basic;

			return;
		}

		/*
		 * Validate if file is required and provided.
		 */
		if (
			( empty( $_FILES[ $input_name ]['tmp_name'] ) || 4 === $_FILES[ $input_name ]['error'] ) &&
		     $this->is_required()
		) {
			wpforms()->process->errors[ $this->form_id ][ $this->field_id ] = wpforms_get_required_label();

			return;
		}

		/*
		 * Validate file size.
		 */
		$validated_size = $this->validate_size();
		if ( ! empty( $validated_size ) ) {
			wpforms()->process->errors[ $this->form_id ][ $this->field_id ] = $validated_size;

			return;
		}

		/*
		 * Validate file extension.
		 */
		$ext = strtolower( pathinfo( $_FILES[ $input_name ]['name'], PATHINFO_EXTENSION ) );

		$validated_ext = $this->validate_extension( $ext );
		if ( ! empty( $validated_ext ) ) {
			wpforms()->process->errors[ $this->form_id ][ $this->field_id ] = $validated_ext;

			return;
		}

		/*
		 * Validate file against what WordPress is set to allow.
		 * At the end of the day, if you try to upload a file that WordPress
		 * doesn't allow, we won't allow it either. Users can use a plugin to
		 * filter the allowed mime types in WordPress if this is an issue.
		 */
		$validated_filetype = $this->validate_wp_filetype_and_ext( $_FILES[ $input_name ]['tmp_name'], sanitize_file_name( wp_unslash( $_FILES[ $input_name ]['name'] ) ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
		if ( ! empty( $validated_filetype ) ) {
			wpforms()->process->errors[ $this->form_id ][ $this->field_id ] = $validated_filetype;

			return;
		}
	}

	/**
	 * Validate modern file uploader field data.
	 *
	 * @since 1.5.6
	 *
	 * @param string $input_name Input name inside the form on front-end.
	 */
	protected function validate_modern( $input_name ) {

		$value = '';

		if ( ! empty( $_POST[ $input_name ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
			$value = json_decode( wp_unslash( $_POST[ $input_name ] ), true ); // phpcs:ignore WordPress.Security
		}

		if ( ! empty( $value ) && ! $this->has_missing_tmp_file( $value ) ) {
			wpforms()->process->errors[ $this->form_id ][ $this->field_id ] = $this->validate_basic( 7 );

			return;
		}

		if ( $this->is_required() && empty( $value ) ) {
			wpforms()->process->errors[ $this->form_id ][ $this->field_id ] = wpforms_get_required_label();
		}
	}

	/**
	 * Check if file(s) exists in temp directory.
	 *
	 * @since 1.6.5
	 *
	 * @param array $files List of files.
	 *
	 * @return bool
	 */
	private function has_missing_tmp_file( $files ) {

		foreach ( $files as $file ) {
			if ( empty( $file['file'] ) || ! is_file( trailingslashit( $this->get_tmp_dir() ) . $file['file'] ) ) {
				return false;
			}
		}

		return true;
	}

	/**
	 * Format and sanitize field.
	 *
	 * @since 1.0.0
	 *
	 * @param int   $field_id     Field ID.
	 * @param array $field_submit Submitted field value.
	 * @param array $form_data    Form data and settings.
	 */
	public function format( $field_id, $field_submit, $form_data ) {

		// Setup class properties to reuse everywhere.
		$this->form_data  = (array) $form_data;
		$this->form_id    = absint( $this->form_data['id'] );
		$this->field_id   = absint( $field_id );
		$this->field_data = $this->form_data['fields'][ $this->field_id ];

		$field_label = ! empty( $this->form_data['fields'][ $this->field_id ]['label'] ) ? $this->form_data['fields'][ $this->field_id ]['label'] : '';
		$input_name  = sprintf( 'wpforms_%d_%d', $this->form_id, $this->field_id );
		$style       = ! empty( $this->field_data['style'] ) ? $this->field_data['style'] : self::STYLE_CLASSIC;

		if ( self::STYLE_CLASSIC === $style ) {
			$this->format_classic( $field_label, $input_name );
		} else {
			$this->format_modern( $field_label, $input_name );
		}
	}

	/**
	 * Format and sanitize classic style of file upload field.
	 *
	 * @since 1.5.6
	 *
	 * @param string $field_label Field label.
	 * @param string $input_name  Input name inside the form on front-end.
	 */
	protected function format_classic( $field_label, $input_name ) {

		$file = ! empty( $_FILES[ $input_name ] ) ? $_FILES[ $input_name ] : false; // phpcs:ignore

		// Preserve field CL visibility state before processing the field.
		$visible = isset( wpforms()->process->fields[ $this->field_id ]['visible'] ) ? wpforms()->process->fields[ $this->field_id ]['visible'] : false;

		// If there was no file uploaded or if this field has conditional logic
		// rules active, stop here before we continue with the
		// upload process.
		if ( ! $file || 0 !== $file['error'] || in_array( $this->field_id, $this->form_data['conditional_fields'], true ) ) {

			wpforms()->process->fields[ $this->field_id ] = array(
				'name'          => sanitize_text_field( $field_label ),
				'value'         => '',
				'file'          => '',
				'file_original' => '',
				'ext'           => '',
				'id'            => absint( $this->field_id ),
				'type'          => $this->type,
			);
			if ( $visible ) {
				wpforms()->process->fields[ $this->field_id ]['visible'] = $visible;
			}

			return;
		}

		// Define data.
		$file_name     = sanitize_file_name( $file['name'] );
		$file_ext      = pathinfo( $file_name, PATHINFO_EXTENSION );
		$file_base     = $this->get_file_basename( $file_name, $file_ext );
		$file_name_new = sprintf( '%s-%s.%s', $file_base, wp_hash( wp_rand() . microtime() . $this->form_id . $this->field_id ), strtolower( $file_ext ) );
		$upload_dir    = wpforms_upload_dir();
		$upload_path   = $upload_dir['path'];

		// Old dir.
		$form_directory   = absint( $this->form_id ) . '-' . md5( $this->form_id . $this->form_data['created'] );
		$upload_path_form = trailingslashit( $upload_path ) . $form_directory;

		// Check for form upload directory destination.
		if ( ! file_exists( $upload_path_form ) ) {

			// New one.
			$form_directory   = absint( $this->form_id ) . '-' . wp_hash( $this->form_data['created'] . $this->form_id );
			$upload_path_form = trailingslashit( $upload_path ) . $form_directory;

			// Check once again and make directory if it's not exists.
			if ( ! file_exists( $upload_path_form ) ) {
				wp_mkdir_p( $upload_path_form );
			}
		}
		$file_new      = trailingslashit( $upload_path_form ) . $file_name_new;
		$file_name_new = wp_basename( trailingslashit( dirname( $file_new ) ) . $file_name_new );
		$file_new      = trailingslashit( dirname( $file_new ) ) . $file_name_new;
		$file_url      = trailingslashit( $upload_dir['url'] ) . trailingslashit( $form_directory ) . $file_name_new;
		$attachment_id = '0';

		// Check if the .htaccess exists in the upload directory, if not - create it.
		wpforms_create_upload_dir_htaccess_file();

		// Check if the index.html exists in the directories, if not - create it.
		wpforms_create_index_html_file( $upload_path );
		wpforms_create_index_html_file( $upload_path_form );

		// Move the file to the uploads dir - similar to _wp_handle_upload().
		$move_new_file = @move_uploaded_file( $file['tmp_name'], $file_new ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
		if ( false === $move_new_file ) {
			wpforms_log(
				'Upload Error, could not upload file',
				$file_url,
				array(
					'type'    => array( 'entry', 'error' ),
					'form_id' => $this->form_data['id'],
				)
			);

			return;
		}

		$this->set_file_fs_permissions( $file_new );

		// Maybe move file to the WordPress media library.
		if ( $this->is_media_integrated() ) {

			// Include necessary code from core.
			require_once ABSPATH . 'wp-admin/includes/media.php';
			require_once ABSPATH . 'wp-admin/includes/file.php';
			require_once ABSPATH . 'wp-admin/includes/image.php';

			// Copy our file into WordPress uploads.
			$file_args = array(
				'error'    => '',
				'tmp_name' => $file_new,
				'name'     => $file_name_new,
				'type'     => $file['type'],
				'size'     => $file['size'],
			);
			$upload    = wp_handle_sideload( $file_args, array( 'test_form' => false ) );

			if ( ! empty( $upload['file'] ) ) {
				// Create a Media attachment for the file.
				$attachment_id = wp_insert_attachment(
					array(
						'post_title'     => $this->get_wp_media_file_title( $file ),
						'post_content'   => $this->get_wp_media_file_desc( $file ),
						'post_status'    => 'publish',
						'post_mime_type' => $file['type'],
					),
					$upload['file']
				);

				if ( ! empty( $attachment_id ) ) {
					// Generate attachment meta.
					wp_update_attachment_metadata(
						$attachment_id,
						wp_generate_attachment_metadata( $attachment_id, $upload['file'] )
					);

					// Update file url/name.
					$file_url      = wp_get_attachment_url( $attachment_id );
					$file_name_new = wp_basename( $file_url );
				}
			}
		}

		// Set final field details.
		wpforms()->process->fields[ $this->field_id ] = array(
			'name'           => sanitize_text_field( $field_label ),
			'value'          => esc_url_raw( $file_url ),
			'file'           => $file_name_new,
			'file_original'  => $file_name,
			'file_user_name' => sanitize_text_field( $file['name'] ),
			'ext'            => $file_ext,
			'attachment_id'  => absint( $attachment_id ),
			'id'             => absint( $this->field_id ),
			'type'           => $this->type,
		);

		// Save field CL visibility state after field processing.
		if ( $visible ) {
			wpforms()->process->fields[ $this->field_id ]['visible'] = $visible;
		}
	}

	/**
	 * Format and sanitize modern style of file upload field.
	 *
	 * @since 1.5.6
	 *
	 * @param string $field_label Field label.
	 * @param string $input_name  Input name inside the form on front-end.
	 */
	protected function format_modern( $field_label, $input_name ) {

		$processed = array(
			'name'      => sanitize_text_field( $field_label ),
			'value'     => '',
			'value_raw' => '',
			'id'        => $this->field_id,
			'type'      => $this->type,
			'style'     => self::STYLE_MODERN,
		);

		// Preserve field CL visibility state before processing the field.
		if ( isset( wpforms()->process->fields[ $this->field_id ]['visible'] ) ) {
			$processed['visible'] = wpforms()->process->fields[ $this->field_id ]['visible'];
		}

		// We should actually receive some files info.
		if ( empty( $_POST[ $input_name ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
			wpforms()->process->fields[ $this->field_id ] = $processed;

			return;
		}

		if ( ! empty( wpforms()->process->fields[ $this->field_id ] ) ) {
			return;
		}

		// Make sure json_decode() doesn't fail on newer PHP.
		try {
			$raw_files = json_decode( wp_unslash( $_POST[ $input_name ] ), true ); // phpcs:ignore WordPress.Security
		} catch ( Exception $e ) {
			wpforms()->process->fields[ $this->field_id ] = $processed;

			return;
		}

		// Make sure we actually have some files.
		if ( empty( $raw_files ) || ! is_array( $raw_files ) ) {
			wpforms()->process->fields[ $this->field_id ] = $processed;

			return;
		}

		// Make sure we process only submitted files with the expected structure and keys.
		$files = array_filter(
			$raw_files,
			static function ( $file ) {

				return is_array( $file ) && count( $file ) === 3 && ! empty( $file['file'] ) && ! empty( $file['name'] );
			}
		);

		if ( empty( $files ) ) {
			wpforms()->process->fields[ $this->field_id ] = $processed;

			return;
		}

		wpforms_create_upload_dir_htaccess_file();

		$upload_dir = wpforms_upload_dir();
		if ( empty( $upload_dir['error'] ) ) {
			wpforms_create_index_html_file( $upload_dir['path'] );
		}

		$data = array();

		foreach ( $files as $file ) {
			$file = $this->generate_file_info( $file );

			if ( $this->is_media_integrated() ) {
				$file['path'] = $file['tmp_path'];

				$file = $this->generate_file_attachment( $file );
			} else {
				// Create form upload directory if needed.
				$this->create_dir( dirname( $file['path'] ) );
				rename( $file['tmp_path'], $file['path'] );
				$this->set_file_fs_permissions( $file['path'] );
			}

			$data[] = $this->generate_file_data( $file );
		}

		if ( ! empty( $data ) ) {
			$processed = wp_parse_args(
				array(
					'value_raw' => $data,
					'value'     => wpforms_chain( $data )
						->map(
							static function ( $file ) {

								return $file['value'];
							}
						)
						->implode( "\n" )
						->value(),
				),
				$processed
			);
		}

		wpforms()->process->fields[ $this->field_id ] = $processed;
	}

	/**
	 * Add additional information to the files array for each file.
	 *
	 * @since 1.5.6
	 *
	 * @param array $file Submitted file basic info.
	 *
	 * @return array
	 */
	protected function generate_file_info( $file ) {

		$dir = $this->get_form_files_dir();

		$file['tmp_path'] = trailingslashit( $this->get_tmp_dir() ) . $file['file'];
		$file['type']     = 'application/octet-stream';

		if ( is_file( $file['tmp_path'] ) ) {
			$filetype     = wp_check_filetype( $file['tmp_path'] );
			$file['type'] = $filetype['type'];
		}

		// Data for no media case.
		$file_ext              = pathinfo( $file['name'], PATHINFO_EXTENSION );
		$file_base             = $this->get_file_basename( $file['name'], $file_ext );
		$file['file_name_new'] = sanitize_file_name( sprintf( '%s-%s.%s', $file_base, wp_hash( wp_rand() . microtime() . $this->form_data['id'] . $this->field_id ), strtolower( $file_ext ) ) );
		$file['file_url']      = trailingslashit( $dir['url'] ) . $file['file_name_new'];
		$file['path']          = trailingslashit( $dir['path'] ) . $file['file_name_new'];
		$file['attachment_id'] = 0;

		return $file;
	}

	/**
	 * Get file basename.
	 *
	 * Ensure the file name length does not exceed 64 characters to prevent `make_clickable()`
	 * function from generating an incorrect URL.
	 *
	 * @since 1.6.5
	 *
	 * @param string $file_name File name.
	 * @param string $file_ext  File extension.
	 *
	 * @return string
	 */
	private function get_file_basename( $file_name, $file_ext ) {

		return mb_substr( wp_basename( $file_name, '.' . $file_ext ), 0, 64, 'UTF-8' );
	}

	/**
	 * Create a Media Library attachment.
	 *
	 * @since 1.5.6
	 *
	 * @param array $file File to create Media Library attachment for.
	 *
	 * @return array
	 */
	protected function generate_file_attachment( $file ) {

		// Include necessary code from core.
		require_once ABSPATH . 'wp-admin/includes/media.php';
		require_once ABSPATH . 'wp-admin/includes/file.php';
		require_once ABSPATH . 'wp-admin/includes/image.php';

		$file_args = array(
			'error'    => '',
			'tmp_name' => $file['path'],
			'name'     => $file['file_name_new'],
			'type'     => $file['type'],
		);
		$upload    = wp_handle_sideload( $file_args, array( 'test_form' => false ) );

		if ( empty( $upload['file'] ) ) {
			return $file;
		}

		// Create a Media attachment for the file.
		$attachment_id = wp_insert_attachment(
			array(
				'post_title'     => $this->get_wp_media_file_title( $file ),
				'post_content'   => $this->get_wp_media_file_desc( $file ),
				'post_status'    => 'publish',
				'post_mime_type' => $file['type'],
			),
			$upload['file']
		);

		if ( empty( $attachment_id ) ) {
			return $file;
		}

		// Generate and update attachment meta.
		wp_update_attachment_metadata(
			$attachment_id,
			wp_generate_attachment_metadata( $attachment_id, $upload['file'] )
		);

		// Update file information.
		$file_url              = wp_get_attachment_url( $attachment_id );
		$file['path']          = $upload['file'];
		$file['file_url']      = $file_url;
		$file['file_name_new'] = wp_basename( $file_url );
		$file['attachment_id'] = $attachment_id;

		return $file;
	}

	/**
	 * Generate an attachment title used in WP Media library for an uploaded file.
	 *
	 * @since 1.6.1
	 *
	 * @param array $file File data.
	 *
	 * @return string
	 */
	private function get_wp_media_file_title( $file ) {

		$title = apply_filters(
			'wpforms_field_' . $this->type . '_media_file_title',
			sprintf(
				'%s: %s',
				$this->field_data['label'],
				$file['name']
			),
			$file,
			$this->field_data
		);

		return wpforms_sanitize_text_deeply( $title );
	}

	/**
	 * Generate an attachment description used in WP Media library for an uploaded file.
	 *
	 * @since 1.6.1
	 *
	 * @param array $file File data.
	 *
	 * @return string
	 */
	private function get_wp_media_file_desc( $file ) {

		$desc = apply_filters(
			'wpforms_field_' . $this->type . '_media_file_desc',
			$this->field_data['description'],
			$file,
			$this->field_data
		);

		return wp_kses_post_deep( $desc );
	}

	/**
	 * Generate a ready for DB data for each file.
	 *
	 * @since 1.5.6
	 *
	 * @param array $file File to generate data for.
	 *
	 * @return array
	 */
	protected function generate_file_data( $file ) {

		return array(
			'name'           => sanitize_text_field( $file['name'] ),
			'value'          => esc_url_raw( $file['file_url'] ),
			'file'           => $file['file_name_new'],
			'file_original'  => $file['name'],
			'file_user_name' => sanitize_text_field( $file['file_user_name'] ),
			'ext'            => wpforms_chain( $file['file'] )->explode( '.' )->pop()->value(),
			'attachment_id'  => isset( $file['attachment_id'] ) ? absint( $file['attachment_id'] ) : 0,
			'id'             => $this->field_id,
			'type'           => $file['type'],
		);
	}

	/**
	 * Format, sanitize, and upload files for fields that have conditional logic rules applied.
	 *
	 * @since 1.3.8
	 *
	 * @param array $form_data Form data and settings.
	 */
	public function format_conditional( $form_data ) {

		// If the form contains no fields with conditional logic no need to
		// continue processing.
		if ( empty( $form_data['conditional_fields'] ) ) {
			return;
		}

		// Loop through each field that has conditional logic rules.
		foreach ( $form_data['conditional_fields'] as $key => $field_id ) {

			// Check if the field exists.
			if ( empty( wpforms()->process->fields[ $field_id ] ) ) {
				continue;
			}

			// Check if the 'type' exists.
			if ( empty( wpforms()->process->fields[ $field_id ]['type'] ) ) {
				continue;
			}

			// We are only concerned with file upload fields.
			if ( wpforms()->process->fields[ $field_id ]['type'] !== $this->type ) {
				continue;
			}

			// If the upload field was no visible at submit then ignore it.
			if ( empty( wpforms()->process->fields[ $field_id ]['visible'] ) ) {
				continue;
			}

			// If there are errors pertaining to this form, its not going to
			// process, so bail and avoid file upload.
			if ( ! empty( wpforms()->process->errors[ $form_data['id'] ] ) ) {
				continue;
			}

			/*
			 * We made it this far, so we can assume we have a file upload field
			 * which was visible during submit, inside a form which does not
			 * contain any errors, so at last we can proceed with uploading the
			 * file.
			 */

			// Unset this field from conditional fields so the format method will proceed.
			unset( $form_data['conditional_fields'][ $key ] );

			// Upload the file and celebrate.
			$this->format( $field_id, array(), $form_data );
		}
	}

	/**
	 * Determine the max allowed file size in bytes as per field options.
	 *
	 * @since 1.0.0
	 *
	 * @return int Number of bytes allowed.
	 */
	public function max_file_size() {

		if ( ! empty( $this->field_data['max_size'] ) ) {

			// Strip any suffix provided (eg M, MB etc), which leaves us with the raw MB value.
			$max_size = preg_replace( '/[^0-9.]/', '', $this->field_data['max_size'] );
			$max_size = wpforms_size_to_bytes( $max_size . 'M' );

		} else {
			$max_size = wpforms_max_upload( true );
		}

		return $max_size;
	}

	/**
	 * Clean up the tmp folder - remove all old files every day (filterable interval).
	 *
	 * @since 1.5.6
	 */
	protected function clean_tmp_files() {

		$files = glob( trailingslashit( $this->get_tmp_dir() ) . '*' );

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

		$lifespan = (int) apply_filters( 'wpforms_field_' . $this->type . '_clean_tmp_files_lifespan', DAY_IN_SECONDS );

		foreach ( $files as $file ) {
			if ( 'index.html' === $file || ! is_file( $file ) ) {
				continue;
			}

			// In some cases filemtime() can return false, in that case - pretend this is a new file and do nothing.
			$modified = (int) filemtime( $file );
			if ( empty( $modified ) ) {
				$modified = time();
			}

			if ( ( time() - $modified ) >= $lifespan ) {
				@unlink( $file ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
			}
		}
	}

	/**
	 * Remove the file from the temporary directory.
	 *
	 * @since 1.5.6
	 */
	public function ajax_modern_remove() {

		$default_error = esc_html__( 'Something went wrong while removing the file.', 'wpforms' );

		$validated_form_field = $this->ajax_validate_form_field_modern();
		if ( empty( $validated_form_field ) ) {
			wp_send_json_error( $default_error, 400 );
		}

		if ( empty( $_POST['file'] ) ) {
			wp_send_json_error( $default_error, 403 );
		}

		$file     = sanitize_file_name( wp_unslash( $_POST['file'] ) );
		$tmp_path = wp_normalize_path( $this->get_tmp_dir() . '/' . $file );

		// Requested file does not exist, which is good.
		if ( ! is_file( $tmp_path ) ) {
			wp_send_json_success( $file );
		}

		if ( @unlink( $tmp_path ) ) {
			wp_send_json_success( $file );
		}

		wp_send_json_error( $default_error, 400 );
	}

	/**
	 * Upload the file, used during AJAX requests.
	 *
	 * @deprecated 1.6.2
	 *
	 * @since 1.5.6
	 */
	public function ajax_modern_upload() {

		$default_error = esc_html__( 'Something went wrong, please try again.', 'wpforms' );

		$validated_form_field = $this->ajax_validate_form_field_modern();
		if ( empty( $validated_form_field ) ) {
			wp_send_json_error( $default_error, 403 );
		}

		// Make sure we have required values from $_FILES.
		if ( empty( $_FILES['file']['name'] ) ) {
			wp_send_json_error( $default_error, 403 );
		}
		if ( empty( $_FILES['file']['tmp_name'] ) ) {
			wp_send_json_error( $default_error, 403 );
		}

		$error          = empty( $_FILES['file']['error'] ) ? 0 : (int) $_FILES['file']['error'];
		$name           = sanitize_file_name( wp_unslash( $_FILES['file']['name'] ) );
		$file_user_name = sanitize_text_field( wp_unslash( $_FILES['file']['name'] ) );
		$path           = $_FILES['file']['tmp_name']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		$extension      = strtolower( pathinfo( $name, PATHINFO_EXTENSION ) );
		$errors         = wpforms_chain( array() )
			->array_merge( (array) $this->validate_basic( $error ) )
			->array_merge( (array) $this->validate_size() )
			->array_merge( (array) $this->validate_extension( $extension ) )
			->array_merge( (array) $this->validate_wp_filetype_and_ext( $path, $name ) )
			->array_filter()
			->value();

		if ( count( $errors ) ) {
			wp_send_json_error( implode( ',', $errors ), 400 );
		}

		$tmp_dir  = $this->get_tmp_dir();
		$tmp_name = $this->get_tmp_file_name( $extension );
		$tmp_path = wp_normalize_path( $tmp_dir . '/' . $tmp_name );
		$tmp      = $this->move_file( $path, $tmp_path );

		if ( ! $tmp ) {
			wp_send_json_error( $default_error, 400 );
		}

		$this->clean_tmp_files();

		wp_send_json_success(
			array(
				'name'           => $name,
				'file'           => pathinfo( $tmp, PATHINFO_FILENAME ) . '.' . pathinfo( $tmp, PATHINFO_EXTENSION ),
				'file_user_name' => $file_user_name,
			)
		);
	}

	/**
	 * Initializes the chunk upload process.
	 *
	 * No data is being send by the client, they expecting an authorization from this method
	 * before sending any chunk.
	 *
	 * The server may return different configs to the uploader client (smaller chunks, disable
	 * parallel uploads etc).
	 *
	 * This method would validate the file extension, maximum size and other things.
	 *
	 * @since 1.6.2
	 */
	public function ajax_chunk_upload_init() {

		$default_error = esc_html__( 'Something went wrong, please try again.', 'wpforms' );

		$validated_form_field = $this->ajax_validate_form_field_modern();
		if ( empty( $validated_form_field ) ) {
			wp_send_json_error( $default_error );
		}

		$handler = Chunk::from_current_request( $this );
		if ( ! $handler || ! $handler->create_metadata() ) {
			wp_send_json_error( $default_error, 403 );
		}

		$error     = 0;
		$name      = sanitize_file_name( wp_unslash( $handler->get_file_name() ) );
		$extension = strtolower( pathinfo( $name, PATHINFO_EXTENSION ) );
		$errors    = wpforms_chain( array() )
			->array_merge( (array) $this->validate_basic( $error ) )
			->array_merge( (array) $this->validate_size( [ $handler->get_file_size() ] ) )
			->array_merge( (array) $this->validate_extension( $extension ) )
			->array_filter()
			->value();

		if ( count( $errors ) > 0 ) {
			wp_send_json_error( implode( ',', $errors ) );
		}

		wp_send_json(
			[
				'success' => true,
				'data'    => [
					'dzchunksize'          => $handler->get_chunk_size(),
					'parallelChunkUploads' => apply_filters( 'wpforms_file_upload_chunk_parallel', true ),
				],
			]
		);
	}

	/**
	 * Upload the files using chunks.
	 *
	 * @since 1.6.2
	 */
	public function ajax_chunk_upload() {

		$default_error = esc_html__( 'Something went wrong, please try again.', 'wpforms' );

		$validated_form_field = $this->ajax_validate_form_field_modern();
		if ( empty( $validated_form_field ) ) {
			wp_send_json_error( $default_error );
		}

		$handler = Chunk::from_current_request( $this );

		if ( ! $handler || ! $handler->load_metadata() ) {
			wp_send_json_error( $default_error, 403 );
		}

		if ( ! $handler->write() ) {
			wp_send_json_error( $default_error, 403 );
		}

		wp_send_json( [ 'success' => true ] );
	}

	/**
	 * Ajax handler for finalizing a chunked upload.
	 *
	 * @since 1.6.2
	 */
	public function ajax_chunk_upload_finalize() {
		$default_error = esc_html__( 'Something went wrong, please try again.', 'wpforms' );

	 	$handler = Chunk::from_current_request( $this );
		if ( ! $handler || ! $handler->load_metadata() ) {
			wp_send_json_error( $default_error, 403 );
		}

		$file_name      = $handler->get_file_name();
		$file_user_name = $handler->get_file_user_name();
		$extension      = strtolower( pathinfo( $file_name, PATHINFO_EXTENSION ) );
		$tmp_dir        = $this->get_tmp_dir();
		$tmp_name       = $this->get_tmp_file_name( $extension );
		$tmp_path       = wp_normalize_path( $tmp_dir . '/' . $tmp_name );

		if ( ! $handler->finalize( $tmp_path ) ) {
			wp_send_json_error( $default_error, 403 );
		}

		$is_valid_type = $this->validate_wp_filetype_and_ext( $tmp_path, $file_name );
		if ( false !== $is_valid_type ) {
			wp_send_json_error( $is_valid_type, 403 );
		}

		$this->clean_tmp_files();

		wp_send_json_success(
			array(
				'name'           => $file_name,
				'file'           => pathinfo( $tmp_path, PATHINFO_FILENAME ) . '.' . pathinfo( $tmp_path, PATHINFO_EXTENSION ),
				'file_user_name' => $file_user_name,
			)
		);
	}

	/**
	 * Validate form ID, field ID and field style for existence and that they are actually valid.
	 *
	 * @since 1.5.6
	 *
	 * @return array Empty array on any kind of failure.
	 */
	protected function ajax_validate_form_field_modern() {

		if (
			empty( $_POST['form_id'] ) ||
			empty( $_POST['field_id'] )
		) {
			return array();
		}

		// phpcs:ignore WordPress.Security.NonceVerification.Missing
		$form_data = wpforms()->form->get( (int) $_POST['form_id'], [ 'content_only' => true ] );

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

		// phpcs:ignore WordPress.Security.NonceVerification.Missing
		$field_id = (int) $_POST['field_id'];
		if (
			! isset( $form_data['fields'][ $field_id ]['style'] ) ||
			self::STYLE_MODERN !== $form_data['fields'][ $field_id ]['style']
		) {
			return array();
		}

		// Make data available everywhere in the class, so we don't need to pass it manually.
		$this->form_data  = $form_data;
		$this->form_id    = $this->form_data['id'];
		$this->field_id   = $field_id;
		$this->field_data = $this->form_data['fields'][ $this->field_id ];

		return array(
			'form_data' => $form_data,
			'field_id'  => $field_id,
		);
	}

	/**
	 * Basic file upload validation.
	 *
	 * @since 1.5.6
	 *
	 * @param int $error Error ID provided by PHP.
	 *
	 * @return false|string False if no errors found, error text otherwise.
	 */
	protected function validate_basic( $error ) {

		if ( 0 === $error || 4 === $error ) {
			return false;
		}

		$errors = array(
			false,
			esc_html__( 'The uploaded file exceeds the upload_max_filesize directive in php.ini.', 'wpforms' ),
			esc_html__( 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.', 'wpforms' ),
			esc_html__( 'The uploaded file was only partially uploaded.', 'wpforms' ),
			esc_html__( 'No file was uploaded.', 'wpforms' ),
			'',
			esc_html__( 'Missing a temporary folder.', 'wpforms' ),
			esc_html__( 'Failed to write file to disk.', 'wpforms' ),
			esc_html__( 'File upload stopped by extension.', 'wpforms' ),
		);

		if ( array_key_exists( $error, $errors ) ) {
			/* translators: %s - error text. */
			return sprintf( esc_html__( 'File upload error. %s', 'wpforms' ), $errors[ $error ] );
		}

		return false;
	}

	/**
	 * Validate file size.
	 *
	 * @since 1.5.6
	 *
	 * @param array $sizes Array with all file sizes in bytes.
	 *
	 * @return false|string False if no errors found, error text otherwise.
	 */
	protected function validate_size( $sizes = null ) {

		if (
			null === $sizes &&
			! empty( $_FILES )
		) {
			$sizes = [];
			foreach ( $_FILES as $file ) {
				$sizes[] = $file['size'];
			}
		}

		if ( ! is_array( $sizes ) ) {
			return false;
		}

		$max_size = min( wp_max_upload_size(), $this->max_file_size() );

		foreach ( $sizes as $size ) {
			if ( $size > $max_size ) {
				return sprintf( /* translators: $s - allowed file size in Mb. */
					esc_html__( 'File exceeds max size allowed (%s).', 'wpforms' ),
					size_format( $max_size )
				);
			}
		}

		return false;
	}

	/**
	 * Validate extension against denylist and admin-provided list.
	 * There are certain extensions we do not allow under any circumstances,
	 * with no exceptions, for security purposes.
	 *
	 * @since 1.5.6
	 *
	 * @param string $ext Extension.
	 *
	 * @return false|string False if no errors found, error text otherwise.
	 */
	protected function validate_extension( $ext ) {

		// Make sure file has an extension first.
		if ( empty( $ext ) ) {
			return esc_html__( 'File must have an extension.', 'wpforms' );
		}

		// Validate extension against all allowed values.
		if ( ! in_array( $ext, $this->get_extensions(), true ) ) {
			return esc_html__( 'File type is not allowed.', 'wpforms' );
		}

		return false;
	}

	/**
	 * Validate file against what WordPress is set to allow.
	 * At the end of the day, if you try to upload a file that WordPress
	 * doesn't allow, we won't allow it either. Users can use a plugin to
	 * filter the allowed mime types in WordPress if this is an issue.
	 *
	 * @since 1.5.6
	 *
	 * @param string $path Path to a newly uploaded file.
	 * @param string $name Name of a newly uploaded file.
	 *
	 * @return false|string False if no errors found, error text otherwise.
	 */
	protected function validate_wp_filetype_and_ext( $path, $name ) {

		$wp_filetype = wp_check_filetype_and_ext( $path, $name );

		$ext             = empty( $wp_filetype['ext'] ) ? '' : $wp_filetype['ext'];
		$type            = empty( $wp_filetype['type'] ) ? '' : $wp_filetype['type'];
		$proper_filename = empty( $wp_filetype['proper_filename'] ) ? '' : $wp_filetype['proper_filename'];

		if ( $proper_filename || ! $ext || ! $type ) {
			return esc_html__( 'File type is not allowed.', 'wpforms' );
		}

		return false;
	}

	/**
	 * Get form-specific uploads directory path and URL.
	 *
	 * @since 1.5.6
	 *
	 * @return array
	 */
	protected function get_form_files_dir() {

		$upload_dir = wpforms_upload_dir();
		$folder     = absint( $this->form_data['id'] ) . '-' . wp_hash( $this->form_data['created'] . $this->form_data['id'] );

		return array(
			'path' => trailingslashit( $upload_dir['path'] ) . $folder,
			'url'  => trailingslashit( $upload_dir['url'] ) . $folder,
		);
	}

	/**
	 * Get tmp dir for files.
	 *
	 * @since 1.5.6
	 *
	 * @return string
	 */
	public function get_tmp_dir() {

		$upload_dir = wpforms_upload_dir();
		$tmp_root   = $upload_dir['path'] . '/tmp';

		if ( ! file_exists( $tmp_root ) || ! wp_is_writable( $tmp_root ) ) {
			wp_mkdir_p( $tmp_root );
		}

		// Check if the index.html exists in the directory, if not - create it.
		wpforms_create_index_html_file( $tmp_root );

		return $tmp_root;
	}

	/**
	 * Create both the directory and index.html file in it if any of them doesn't exist.
	 *
	 * @since 1.5.6
	 *
	 * @param string $path Path to the directory.
	 *
	 * @return string Path to the newly created directory.
	 */
	protected function create_dir( $path ) {

		if ( ! file_exists( $path ) ) {
			wp_mkdir_p( $path );
		}

		// Check if the index.html exists in the path, if not - create it.
		wpforms_create_index_html_file( $path );

		return $path;
	}

	/**
	 * Get tmp file name.
	 *
	 * @since 1.5.6
	 *
	 * @param string $extension File extension.
	 *
	 * @return string
	 */
	protected function get_tmp_file_name( $extension ) {

		return wp_hash( wp_rand() . microtime() . $this->form_id . $this->field_id ) . '.' . $extension;
	}

	/**
	 * Move file to a permanent location.
	 *
	 * @since 1.5.6
	 *
	 * @param string $path_from From.
	 * @param string $path_to   To.
	 *
	 * @return false|string False on error.
	 */
	protected function move_file( $path_from, $path_to ) {

		$this->create_dir( dirname( $path_to ) );

		if ( false === move_uploaded_file( $path_from, $path_to ) ) {
			wpforms_log(
				'Upload Error, could not upload file',
				$path_from,
				array(
					'type' => array( 'entry', 'error' ),
				)
			);

			return false;
		}

		$this->set_file_fs_permissions( $path_to );

		return $path_to;
	}

	/**
	 * Set correct file permissions in the file system.
	 *
	 * @since 1.5.6
	 *
	 * @param string $path File to set permissions for.
	 */
	protected function set_file_fs_permissions( $path ) {

		// Set correct file permissions.
		$stat = stat( dirname( $path ) );

		@chmod( $path, $stat['mode'] & 0000666 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
	}

	/**
	 * Get all allowed extensions.
	 * Check against user-entered extensions.
	 *
	 * @since 1.5.6
	 *
	 * @return array
	 */
	protected function get_extensions() {

		// Allowed file extensions by default.
		$default_extensions = $this->get_default_extensions();

		// Allowed file extensions.
		$extensions = ! empty( $this->field_data['extensions'] ) ? explode( ',', $this->field_data['extensions'] ) : $default_extensions;

		return wpforms_chain( $extensions )
			->map(
				static function ( $ext ) {

					return strtolower( preg_replace( '/[^A-Za-z0-9]/', '', $ext ) );
				}
			)
			->array_filter()
			->array_intersect( $default_extensions )
			->value();
	}

	/**
	 * Get default extensions supported by WordPress
	 * without those that we manually denylist.
	 *
	 * @since 1.5.6
	 *
	 * @return array
	 */
	protected function get_default_extensions() {

		return wpforms_chain( get_allowed_mime_types() )
			->array_keys()
			->implode( '|' )
			->explode( '|' )
			->array_diff( $this->denylist )
			->value();
	}

	/**
	 * Whether field is required or not.
	 *
	 * @uses $this->field_data
	 *
	 * @since 1.5.6
	 *
	 * @return bool
	 */
	protected function is_required() {

		return ! empty( $this->field_data['required'] );
	}

	/**
	 * Whether field is integrated with WordPress Media Library.
	 *
	 * @uses $this->field_data
	 *
	 * @since 1.5.6
	 *
	 * @return bool
	 */
	protected function is_media_integrated() {

		return ! empty( $this->field_data['media_library'] ) && '1' === $this->field_data['media_library'];
	}

	/**
	 * Disallow WPForms upload directory indexing in robots.txt.
	 *
	 * @since 1.6.1
	 *
	 * @param string $output Robots.txt output.
	 *
	 * @return string
	 */
	public function disallow_upload_dir_indexing( $output ) {

		$upload_dir = wpforms_upload_dir();

		if ( ! empty( $upload_dir['error'] ) ) {
			return $output;
		}

		$site_url = site_url();

		$upload_root = str_replace( $site_url, '', $upload_dir['url'] );
		$upload_root = trailingslashit( $upload_root );

		$site_url_parts = wp_parse_url( $site_url );
		if ( ! empty( $site_url_parts['path'] ) ) {
			$upload_root = $site_url_parts['path'] . $upload_root;
		}

		$output .= "Disallow: $upload_root\n";

		return $output;
	}
}

new WPForms_Field_File_Upload();
