<?php

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

class GFAsyncUpload {

	public static function upload() {

		GFCommon::log_debug( 'GFAsyncUpload::upload(): Starting.' );

		if ( $_SERVER['REQUEST_METHOD'] !== 'POST' ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated
			self::die_error();
		}

		// If the file is bigger than the server can accept then the form_id might not arrive.
		// This might happen if the file is bigger than the max post size ini setting.
		// Validation in the browser reduces the risk of this happening.
		if ( ! isset( $_REQUEST['form_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			GFCommon::log_debug( 'GFAsyncUpload::upload(): File upload aborted because the form_id was not found. The file may have been bigger than the max post size ini setting.' );
			self::die_error( 500, __( 'Failed to upload file.', 'gravityforms' ) );
		}

		$form_unique_id = rgpost( 'gform_unique_id' );
		if ( ! ctype_alnum( $form_unique_id ) ) {
			self::die_error();
		}

		$form_id = absint( $_REQUEST['form_id'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$form    = GFAPI::get_form( $form_id );

		if ( empty( $form ) || ! $form['is_active'] ) {
			self::die_error();
		}

		$field_id = absint( rgpost( 'field_id' ) );

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

		if ( GFCommon::form_requires_login( $form ) && ! is_user_logged_in() ) {
			self::die_error( 401, __( 'You must be logged in to upload files to this form.', 'gravityforms' ) );
		}

		/**
		 * Filter the field object that will be associated with the uploaded file.
		 *
		 * This is useful when you want to use Gravity Forms' upload system to upload files. Using this filter you can return a one-time-use field object
		 * to process the file as desired.
		 *
		 * @since 2.2.2
		 *
		 * @param GF_Field $field The current field object.
		 * @param array    $form The current form object.
		 * @param int      $field_id The field ID as passed via the $_POST. Used to fetch the current $field.
		 */
		$field = gf_apply_filters( array( 'gform_multifile_upload_field', $form_id, $field_id ), GFFormsModel::get_field( $form, $field_id ), $form, $field_id );

		if ( empty( $field ) || GFFormsModel::get_input_type( $field ) !== 'fileupload' || ! $field->multipleFiles ) {
			self::die_error();
		}

		$target_dir = rgar( GFFormsModel::get_tmp_upload_location( $form_id ), 'path' );
		if ( empty( $target_dir ) || ( ! is_dir( $target_dir ) && ! wp_mkdir_p( $target_dir ) ) ) {
			GFCommon::log_debug( "GFAsyncUpload::upload(): Couldn't create the tmp folder: " . $target_dir );
			self::die_error( 500, __( 'Failed to upload file.', 'gravityforms' ) );
		}

		$time = current_time( 'mysql' );
		$y    = substr( $time, 0, 4 );
		$m    = substr( $time, 5, 2 );

		// Adding index.html files to all subfolders.
		if ( ! file_exists( GFFormsModel::get_upload_root() . 'index.html' ) ) { // get_upload_root returned value includes the trailing slash.
			GFForms::add_security_files();
		} else if ( ! file_exists( GFFormsModel::get_upload_path( $form_id ) . '/index.html' ) ) { // nosemgrep audit.php.lang.security.file.phar-deserialization
			GFCommon::recursive_add_index_file( GFFormsModel::get_upload_path( $form_id ) );
		} else if ( ! file_exists( GFFormsModel::get_upload_path( $form_id ) . "/$y/index.html" ) ) { // nosemgrep audit.php.lang.security.file.phar-deserialization
			GFCommon::recursive_add_index_file( GFFormsModel::get_upload_path( $form_id ) . "/$y" );
		} else if ( is_dir( GFFormsModel::get_upload_path( $form_id ) . "/$y/$m" ) ) { // Prevent adding the index file if the month upload folder is not created yet.
			GFCommon::recursive_add_index_file( GFFormsModel::get_upload_path( $form_id ) . "/$y/$m" );
		}

		if ( ! file_exists( $target_dir . '/index.html' ) ) { // nosemgrep audit.php.lang.security.file.phar-deserialization
			GFCommon::recursive_add_index_file( $target_dir );
		}

		$uploaded_filename = sanitize_file_name( rgar( $_REQUEST, 'original_filename' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
		self::die_if_extension_disallowed( $uploaded_filename, 'original_filename' );

		$file_name = sanitize_file_name( rgar( $_REQUEST, 'name' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
		self::die_if_extension_disallowed( $file_name, 'name' );

		self::die_if_extensions_different( $uploaded_filename, $file_name );

		$allowed_extensions = $field->get_clean_allowed_extensions();
		if ( ! empty( $allowed_extensions ) ) {
			self::die_if_not_allowed_field_extension( $uploaded_filename, 'original_filename', $allowed_extensions );
			self::die_if_not_allowed_field_extension( $file_name, 'name', $allowed_extensions );
		}

		$max_upload_size_in_bytes = $field->get_max_file_size_bytes();
		$max_upload_size_in_mb    = $max_upload_size_in_bytes / 1048576;

		if ( $_FILES['file']['size'] > 0 && $_FILES['file']['size'] > $max_upload_size_in_bytes ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.NonceVerification.Missing
			// translators: %d: Maximum file size in MB.
			self::die_error( 104, sprintf( __( 'File exceeds size limit. Maximum file size: %dMB', 'gravityforms' ), $max_upload_size_in_mb ) );
		}

		$chunk         = isset( $_REQUEST['chunk'] ) ? intval( $_REQUEST['chunk'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$chunks        = isset( $_REQUEST['chunks'] ) ? intval( $_REQUEST['chunks'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$chunk_data    = $chunks && $file_name ? rgar( $_REQUEST, str_replace( '.', '_', $file_name ) ) : array(); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$chunk_data    = is_array( $chunk_data ) ? $chunk_data : array();
		$tmp_file_name = '';
		$write_offset  = 0;

		if ( $chunks < 0 || $chunk < 0 || ( ! $chunks && $chunk ) || ( $chunks && $chunk >= $chunks ) ) {
			self::die_error( 105, __( 'Upload unsuccessful', 'gravityforms' ) . ' ' . $uploaded_filename );
		}

		if ( $chunks && $chunk ) {
			$submitted_tmp_file_name = rgar( $chunk_data, 'temp_filename' );
			$chunk_state             = self::decode_chunk_token( rgar( $chunk_data, 'hash' ) );

			if ( ! self::is_valid_chunk_state( $chunk_state, $submitted_tmp_file_name, $chunk, $form_id, $field_id, $chunks, $uploaded_filename ) ) {
				GFCommon::log_debug( __METHOD__ . sprintf( '(): Invalid hash for chunk #%d.', $chunk ) );
				self::die_error( 105, __( 'Upload unsuccessful', 'gravityforms' ) . ' ' . $uploaded_filename );
			}

			$tmp_file_name = $chunk_state['temp_filename'];
			$write_offset  = $chunk_state['offset'];
		}

		if ( empty( $tmp_file_name ) ) {
			$tmp_file_name = 'gf_' . GFCommon::random_str( 32 ) . '.' . pathinfo( $file_name, PATHINFO_EXTENSION );
		}

		$tmp_file_name = sanitize_file_name( $tmp_file_name );
		if ( ! self::is_valid_temp_filename( $tmp_file_name ) ) {
			self::die_error( 105, __( 'Upload unsuccessful', 'gravityforms' ) . ' ' . $uploaded_filename );
		}

		$file_path = $target_dir . $tmp_file_name;
		if ( $chunks && $chunk && ( ! file_exists( "{$file_path}.part" ) || filesize( "{$file_path}.part" ) !== $write_offset ) ) {
			self::die_error( 105, __( 'Upload unsuccessful', 'gravityforms' ) . ' ' . $uploaded_filename );
		}

		if ( ! $field->is_check_type_and_ext_disabled() && ! $chunks ) {

			$file_array = $_FILES['file']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.NonceVerification.Missing

			self::die_if_invalid_type_and_ext( $file_array, $uploaded_filename, 'original_filename' );
			self::die_if_invalid_type_and_ext( $file_array, $file_name, 'name' );
		}

		$cleanup_target_dir = apply_filters( 'gform_cleanup_target_dir', true ); // Remove old files
		$max_file_age       = 5 * 3600; // Temp file age in seconds

		// Remove old temp files
		if ( $cleanup_target_dir ) {
			if ( is_dir( $target_dir ) && ( $dir = opendir( $target_dir ) ) ) {
				while ( ( $file = readdir( $dir ) ) !== false ) {
					$tmp_file_path = $target_dir . $file;

					// Remove temp file if it is older than the max age and is not the current file
					if ( preg_match( '/\.part$/', $file ) && ( filemtime( $tmp_file_path ) < time() - $max_file_age ) && ( $tmp_file_path != "{$file_path}.part" ) ) { // nosemgrep audit.php.lang.security.file.phar-deserialization
						GFCommon::log_debug( 'GFAsyncUpload::upload(): Deleting file: ' . $tmp_file_path );
						@unlink( $tmp_file_path ); // nosemgrep audit.php.lang.security.file.phar-deserialization, audit.php.lang.security.file.read-write-delete
					}
				}
				closedir( $dir );
			} else {
				GFCommon::log_debug( 'GFAsyncUpload::upload(): Failed to open temp directory: ' . $target_dir );
				self::die_error( 100, __( 'Failed to open temp directory.', 'gravityforms' ) );
			}
		}

		if ( isset( $_SERVER['HTTP_CONTENT_TYPE'] ) ) {
			$contentType = $_SERVER['HTTP_CONTENT_TYPE']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
		}

		if ( isset( $_SERVER['CONTENT_TYPE'] ) ) {
			$contentType = $_SERVER['CONTENT_TYPE']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
		}

		// Handle non multipart uploads older WebKit versions didn't support multipart in HTML5
		if ( strpos( $contentType, 'multipart' ) !== false ) {
			if ( isset( $_FILES['file']['tmp_name'] ) && is_uploaded_file( $_FILES['file']['tmp_name'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Missing
				// Open temp file
				$out = @fopen( "{$file_path}.part", $chunk === 0 ? 'wb' : 'c+b' );
				if ( $out ) {
					if ( $chunk && fseek( $out, $write_offset ) !== 0 ) {
						self::die_error( 102, __( 'Failed to seek output stream.', 'gravityforms' ) );
					}

					// Read binary input stream and write it at the expected offset.
					$in = @fopen( $_FILES['file']['tmp_name'], 'rb' ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Missing

					if ( $in ) {
						while ( $buff = fread( $in, 4096 ) ) {
							$bytes_written = fwrite( $out, $buff ); // nosemgrep audit.php.lang.security.file.read-write-delete
							if ( false === $bytes_written ) {
								self::die_error( 102, __( 'Failed to write output stream.', 'gravityforms' ) );
							}
							$write_offset += $bytes_written;
						}
					} else {
						self::die_error( 101, __( 'Failed to open input stream.', 'gravityforms' ) );
					}

					@fclose( $in );
					@fclose( $out );
					@unlink( $_FILES['file']['tmp_name'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Missing
				} else {
					self::die_error( 102, __( 'Failed to open output stream.', 'gravityforms' ) );
				}
			} else {
				self::die_error( 103, __( 'Failed to move uploaded file.', 'gravityforms' ) );
			}
		} else {
			// Open temp file
			$out = @fopen( "{$file_path}.part", $chunk === 0 ? 'wb' : 'c+b' );
			if ( $out ) {
				if ( $chunk && fseek( $out, $write_offset ) !== 0 ) {
					self::die_error( 102, __( 'Failed to seek output stream.', 'gravityforms' ) );
				}

				// Read binary input stream and write it at the expected offset.
				$in = @fopen( 'php://input', 'rb' );

				if ( $in ) {
					while ( $buff = fread( $in, 4096 ) ) {
						$bytes_written = fwrite( $out, $buff ); // nosemgrep audit.php.lang.security.file.read-write-delete
						if ( false === $bytes_written ) {
							self::die_error( 102, __( 'Failed to write output stream.', 'gravityforms' ) );
						}
						$write_offset += $bytes_written;
					}
				} else {
					self::die_error( 101, __( 'Failed to open input stream.', 'gravityforms' ) );
				}

				@fclose( $in );
				@fclose( $out );
			} else {
				self::die_error( 102, __( 'Failed to open output stream.', 'gravityforms' ) );
			}
		}

		if ( ! $chunks || $chunk == $chunks - 1 ) {
			// Upload is complete. Strip the temp .part suffix off
			if ( ! rename( "{$file_path}.part", $file_path ) ) {
				self::die_error( 105, __( 'Upload unsuccessful', 'gravityforms' ) . ' ' . $uploaded_filename );
			}

			if ( file_exists( $file_path ) ) { // nosemgrep audit.php.lang.security.file.phar-deserialization
				if ( $chunks && ! $field->is_check_type_and_ext_disabled() ) {
					$file_array             = $_FILES['file']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.NonceVerification.Missing
					$file_array['tmp_name'] = $file_path;

					self::die_if_invalid_type_and_ext( $file_array, $uploaded_filename, 'original_filename', $file_path );
					self::die_if_invalid_type_and_ext( $file_array, $file_name, 'name', $file_path );
				}

				GFFormsModel::set_permissions( $file_path );
			} else {
				self::die_error( 105, __( 'Upload unsuccessful', 'gravityforms' ) . ' ' . $uploaded_filename );
			}

			self::send_headers( 200 );
			gf_do_action( array( 'gform_post_multifile_upload', $form['id'] ), $form, $field, $uploaded_filename, $tmp_file_name, $file_path );

			GFCommon::log_debug( sprintf( 'GFAsyncUpload::upload(): File upload complete. temp_filename: %s  uploaded_filename: %s ', $tmp_file_name, $uploaded_filename ) );
		} else {
			if ( file_exists( "{$file_path}.part" ) ) {
				GFFormsModel::set_permissions( "{$file_path}.part" );
			} else {
				self::die_error( 105, __( 'Upload unsuccessful', 'gravityforms' ) . ' ' . $uploaded_filename );
			}

			self::send_headers( 200 );
			GFCommon::log_debug( sprintf( 'GFAsyncUpload::upload(): Chunk upload complete. temp_filename: %s  uploaded_filename: %s chunk: %d', $tmp_file_name, $uploaded_filename, $chunk ) );
		}

		$output = array(
			'status' => 'ok',
			'data'   => array(
				'temp_filename'     => $tmp_file_name,
				'uploaded_filename' => str_replace( "\\'", "'", urldecode( $uploaded_filename ) ), //Decoding filename to prevent file name mismatch.
			),
		);

		if ( $chunks && ( $chunk !== $chunks - 1 ) ) {
			$output['data']['hash'] = self::get_chunk_hash( $tmp_file_name, $chunk + 1, $form_id, $field_id, $uploaded_filename, $write_offset, $chunks );
		}

		$output = json_encode( $output );

		die( $output ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
	}

	/**
	 * Ends the request with an error response.
	 *
	 * @since unknown
	 * @since 2.9.20 Made the params optional.
	 *
	 * @param int|string $status_code The status code. Optional. Defaults to 400.
	 * @param string     $message     The error message. Optional. Defaults to 'Invalid request.'.
	 *
	 * @return void
	 */
	public static function die_error( $status_code = 400, $message = '' ) {
		self::send_headers( is_int( $status_code ) ? $status_code : 400 );
		wp_send_json(
			array(
				'status' => 'error',
				'error'  => array(
					'code'    => $status_code,
					'message' => $message ?: __( 'Invalid request.', 'gravityforms' ),
				),
			)
		);
	}

	/**
	 * Sends the headers for the response.
	 *
	 * @since 2.9.20
	 *
	 * @param int $status_code The status code.
	 *
	 * @return void
	 */
	private static function send_headers( $status_code ) {
		header( 'Content-Type: application/json; charset=' . get_option( 'blog_charset' ) );
		send_nosniff_header();
		nocache_headers();
		status_header( $status_code );
	}

	/**
	 * Ends the request with an error response if the nonce is invalid.
	 *
	 * @since 2.9.24
	 *
	 * @param int $form_id  The form ID.
	 * @param int $field_id The field ID.
	 *
	 * @return void
	 */
	private static function die_if_nonce_invalid( $form_id, $field_id ) {
		$nonce  = rgar( $_REQUEST, "_gform_file_upload_nonce_{$form_id}_{$field_id}" ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$action = "gform_file_upload_{$form_id}_{$field_id}";

		if ( empty( $nonce ) ) {
			/**
			 * The legacy nonce is used by the Chained Selects field in the form editor.
			 *
			 * @depecated 2.9.24
			 * @remove-in 3.0
			 */
			$nonce  = rgar( $_REQUEST, "_gform_file_upload_nonce_{$form_id}" ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			$action = "gform_file_upload_{$form_id}";
		}

		if ( empty( $nonce ) || ! wp_verify_nonce( $nonce, $action ) ) { // nosemgrep scanner.php.wp.security.csrf.nonce-check-not-dying
			self::die_error( 403, __( 'Your session has expired. Please refresh the page and try again.', 'gravityforms' ) );
		}
	}

	/**
	 * Ends the request with an error response if the file extension is disallowed.
	 *
	 * @since 2.9.24
	 *
	 * @param string $file_name  The file name.
	 * @param string $input_name The input name the file name is from.
	 *
	 * @return void
	 */
	private static function die_if_extension_disallowed( $file_name, $input_name ) {
		if ( GFCommon::file_name_has_disallowed_extension( $file_name ) ) {
			GFCommon::log_debug( __METHOD__ . sprintf( '(): Illegal file extension (input: %s): %s', $input_name, $file_name ) );
			self::die_error( 104, __( 'The uploaded file type is not allowed.', 'gravityforms' ) );
		}
	}

	/**
	 * Ends the request with an error response if the file names have different extensions.
	 *
	 * @since 2.9.24
	 *
	 * @param string $uploaded_filename The file name from the uploaded_filename input.
	 * @param string $file_name         The file name from the name input.
	 *
	 * @return void
	 */
	private static function die_if_extensions_different( $uploaded_filename, $file_name ) {
		$uploaded_filename_ext = pathinfo( $uploaded_filename, PATHINFO_EXTENSION );
		$file_name_ext         = pathinfo( $file_name, PATHINFO_EXTENSION );
		if ( $uploaded_filename_ext !== $file_name_ext ) {
			GFCommon::log_debug( __METHOD__ . sprintf( '(): File extensions do not match. uploaded_filename: %s; name: %s', $uploaded_filename_ext, $file_name_ext ) );
			self::die_error( 105, __( 'Upload unsuccessful', 'gravityforms' ) . ' ' . $uploaded_filename );
		}
	}

	/**
	 * Ends the request with an error response if the file extension is not one of the allowed extensions configured on the field.
	 *
	 * @since 2.9.24
	 *
	 * @param string   $file_name  The file name.
	 * @param string   $input_name The input name the file name is from.
	 * @param string[] $extensions The allowed extensions.
	 *
	 * @return void
	 */
	private static function die_if_not_allowed_field_extension( $file_name, $input_name, $extensions ) {
		if ( ! GFCommon::match_file_extension( $file_name, $extensions ) ) {
			GFCommon::log_debug( __METHOD__ . sprintf( '(): The uploaded file type is not allowed (input: %s): %s', $input_name, $file_name ) );
			// translators: %s: list of allowed extensions.
			self::die_error( 104, sprintf( __( 'The uploaded file type is not allowed. Must be one of the following: %s', 'gravityforms' ), implode( ', ', $extensions ) ) );
		}
	}

	/**
	 * Ends the request with an error response if the file type and extension are invalid.
	 *
	 * @since 2.9.24
	 *
	 * @param array  $file                The file details from $_FILES.
	 * @param string $file_name           The file name.
	 * @param string $input_name          The input name the file name is from.
	 * @param string $file_path_to_delete The file path to delete if validation fails.
	 *
	 * @return void
	 */
	private static function die_if_invalid_type_and_ext( $file, $file_name, $input_name, $file_path_to_delete = '' ) {
		$result = GFCommon::check_type_and_ext( $file, $file_name );
		if ( is_wp_error( $result ) ) {
			GFCommon::log_debug( sprintf( '%s(): %s (input: %s); %s; %s', __METHOD__, $file_name, $input_name, $result->get_error_code(), $result->get_error_message() ) );
			if ( ! empty( $file_path_to_delete ) && file_exists( $file_path_to_delete ) ) { // nosemgrep audit.php.lang.security.file.phar-deserialization
				@unlink( $file_path_to_delete ); // nosemgrep audit.php.lang.security.file.read-write-delete
			}
			self::die_error( $result->get_error_code(), $result->get_error_message() );
		}
	}

	/**
	 * Returns a hash created using the given arguments.
	 *
	 * @since 2.9.24
	 * @since 3.0.2.7 Added offset and chunks for better hash security.
	 *
	 * @param string $tmp_file_name     The temporary file name.
	 * @param int    $chunk             The chunk number.
	 * @param int    $form_id           The form ID.
	 * @param int    $field_id          The field ID.
	 * @param string $uploaded_filename The uploaded file name.
	 * @param int    $offset            Bytes written so far.
	 * @param int    $chunks            Total chunk count.
	 *
	 * @return string
	 */
	private static function get_chunk_hash( $tmp_file_name, $chunk, $form_id, $field_id, $uploaded_filename, $offset, $chunks ) {
		$payload = wp_json_encode(
			array(
				'temp_filename'     => (string) $tmp_file_name,
				'next_chunk'        => (int) $chunk,
				'form_id'           => (int) $form_id,
				'field_id'          => (int) $field_id,
				'uploaded_filename' => (string) $uploaded_filename,
				'offset'            => (int) $offset,
				'total_chunks'      => (int) $chunks,
			)
		);

		$encoded_payload = rtrim( strtr( base64_encode( $payload ), '+/', '-_' ), '=' );

		return $encoded_payload . '.' . hash_hmac( 'sha256', 'gravityforms-upload-chunk-v1|' . $encoded_payload, wp_salt( 'auth' ) );
	}

	/**
	 * Decodes and verifies a signed chunk state token.
	 *
	 * @since 3.0.3
	 *
	 * @param mixed $token The signed chunk state token.
	 *
	 * @return array|false
	 */
	private static function decode_chunk_token( $token ) {
		if ( ! is_string( $token ) || substr_count( $token, '.' ) !== 1 ) {
			return false;
		}

		list( $encoded_payload, $signature ) = explode( '.', $token, 2 );
		$expected_signature                  = hash_hmac( 'sha256', 'gravityforms-upload-chunk-v1|' . $encoded_payload, wp_salt( 'auth' ) );
		if ( $encoded_payload === '' || ! hash_equals( $expected_signature, $signature ) ) {
			return false;
		}

		// Restore stripped Base64 padding so the URL-safe token can be decoded, basically how many '=' to add to the end of the string.
		$padding = ( 4 - strlen( $encoded_payload ) % 4 ) % 4;
		$payload = base64_decode( strtr( $encoded_payload, '-_', '+/' ) . str_repeat( '=', $padding ), true );
		$state   = is_string( $payload ) ? json_decode( $payload, true ) : null;

		return is_array( $state ) ? $state : false;
	}

	/**
	 * Determines whether a temporary filename is a safe server-side basename.
	 *
	 * @since 3.0.3
	 *
	 * @param mixed $tmp_file_name Temporary filename.
	 *
	 * @return bool
	 */
	private static function is_valid_temp_filename( $tmp_file_name ) {
		if ( ! is_string( $tmp_file_name ) || $tmp_file_name === '' ) {
			return false;
		}

		if ( sanitize_file_name( $tmp_file_name ) !== $tmp_file_name ) {
			return false;
		}

		if ( wp_basename( $tmp_file_name ) !== $tmp_file_name ) {
			return false;
		}

		if ( GFCommon::file_name_has_disallowed_extension( $tmp_file_name ) ) {
			return false;
		}

		return true;
	}

	/**
	 * Determines whether the supplied chunk state matches the signed token.
	 *
	 * @since 3.0.3
	 *
	 * @param mixed $chunk_state          Decoded chunk state token.
	 * @param mixed $tmp_file_name        Client-supplied temporary filename.
	 * @param int   $chunk                Current chunk number.
	 * @param int   $form_id              Form ID.
	 * @param int   $field_id             Field ID.
	 * @param int   $chunks               Total chunk count.
	 * @param string $uploaded_filename   Uploaded filename.
	 *
	 * @return bool
	 */
	private static function is_valid_chunk_state( $chunk_state, $tmp_file_name, $chunk, $form_id, $field_id, $chunks, $uploaded_filename ) {
		if ( ! self::is_valid_temp_filename( $tmp_file_name ) || ! is_array( $chunk_state ) ) {
			return false;
		}

		$state_form_id       = rgar( $chunk_state, 'form_id' );
		$state_field_id      = rgar( $chunk_state, 'field_id' );
		$state_filename      = rgar( $chunk_state, 'uploaded_filename' );
		$state_temp_filename = rgar( $chunk_state, 'temp_filename' );
		$state_total_chunks  = rgar( $chunk_state, 'total_chunks' );
		$state_next_chunk    = rgar( $chunk_state, 'next_chunk' );
		$state_offset        = rgar( $chunk_state, 'offset' );

		if ( $state_form_id !== $form_id || $state_field_id !== $field_id ) {
			return false;
		}

		if ( $state_filename !== $uploaded_filename || $state_temp_filename !== $tmp_file_name ) {
			return false;
		}

		if ( $state_total_chunks !== $chunks || $state_next_chunk !== $chunk || $state_offset < 0 ) {
			return false;
		}

		return true;
	}
}

GFAsyncUpload::upload();
