Contact Form 7 multiple file input

Needed to add a form field for attaching up to 3 images to a CF7 form – it turns out CF7 doesn’t support <input type="file" multiple /> out of the box.

Skip to full code snippet

To add a file input to a CF7 form you would add a field to the form’s template as such:

Edit Contact Form -> Form
<label> Attachment
    [file images filetypes:jpg|jpeg|png|gif|webp|avif limit:9mb] </label>

Note: In the “Mail” tab you also need to add the tag to “File attachments” for the submitted file to be attached to the e-mail ([images]).

First, I asked AI to write me some code to add the functionality for multiple file uploads. It targeted a file input by its’ static name (so it only works for 1 specific form), used some complicated regexes and included some unnecessary code. I took the slop and mostly rewrote and distilled it down to 3 simple filters that I can reuse on other sites in the future.

The filters

Adding the multiple attribute to a CF7 form tag doesn’t do anything out of the box:

[file images filetypes:jpg|jpeg|png|gif|webp|avif limit:9mb multiple]

Here’s the additional code to make it work:

PHP
/**
 * When processing the `[file]` CF7 form tag, add the `ska-multiple` class to mark any file inputs that should have the `multiple` attribute.
 */
add_filter('wpcf7_form_tag', function(array $tag) {
	if(
		$tag['basetype'] === 'file'
		&& isset($tag['options'])
		&& is_array($tag['options'])
		&& in_array('multiple', $tag['options'], true)
	) {
		$tag['options'][] = 'class:ska-multiple';
	}
	return $tag;
});

/**
 * Filter the final CF7 form output.
 * Add `multiple` attribute to any inputs with the `ska-multiple` class name.
 */
add_filter('wpcf7_form_elements', function(string $content) {
	$p = new WP_HTML_Tag_Processor($content);
	while($p->next_tag(['tag_name' => 'input', 'class_name' => 'ska-multiple'])) {
		$p->set_attribute('multiple', 'multiple');
		$p->set_attribute('name', $p->get_attribute('name') . '[]');
		$p->remove_class('ska-multiple');
	}
	return $p->get_updated_html();
});

First adding a custom class name to “tag” the input, and then using WP_HTML_Tag_Processor to find it in the final output, add the multiple attribute to the <input type="file"> and change the name attribute to an array by appending [].

Next, to limit the maximum amount of files that can be attached I added “support” for 2 more attributes: maxfiles and errortext:

[file images filetypes:jpg|jpeg|png|gif|webp|avif limit:9mb multiple maxfiles:3 errortext "Up to %d files are allowed."]

Here’s the code to validate the amount of files and display an error message when applicable:

PHP
/**
 * @param object $result
 * @param object $tag
 * @return object
 */
function ska_validate_max_files($result, $tag) {

	if(
		$tag->basetype !== 'file'
		|| !property_exists($tag, 'options')
		|| !is_array($tag->options)
		|| !in_array('multiple', $tag->options, true)
		|| !isset($_FILES[$tag->name])
	) {
		return $result;
	}

	$max = array_find($tag->options, function($v) {
		return is_string($v) && str_starts_with($v, 'maxfiles:') && is_numeric(str_replace('maxfiles:', '', $v));
	});
	if(!is_string($max)) {
		return $result;
	}
	$max = absint(str_replace('maxfiles:', '', $max));

	$files = $_FILES[$tag->name] ?? null; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
	if(!is_array($files) || !isset($files['name'])) {
		return $result;
	}

	$names = is_array($files['name']) ? array_filter($files['name']) : [$files['name']];
	if(count($names) > $max) {

		$message = (
			in_array('errortext', $tag->options, true) 
			&& property_exists($tag, 'labels') 
			&& is_array($tag->labels) 
			&& isset($tag->labels[0]) 
			&& is_string($tag->labels[0])
		) ? sanitize_text_field($tag->labels[0]) : 'Up to %d file attachments are permitted.';

		$result->invalidate($tag, esc_html(sprintf($message, $max)));
	}

	return $result;
}
add_filter('wpcf7_validate_file', 'ska_validate_max_files', 10, 2);
add_filter('wpcf7_validate_file*', 'ska_validate_max_files', 10, 2);

AI also told me to remove the field tag from the “File attachments” section of the contact form configuration and use this filter to attach the multiple files:

Superfluous code from AIPHP
add_filter('wpcf7_mail_components', function($components, $contact_form, $mail) {

	$submission = WPCF7_Submission::get_instance();
	if(!$submission) {
		return $components;
	}

	$uploaded_files = $submission->uploaded_files();
	if(!isset($uploaded_files['images']) || empty($uploaded_files['images'])) {
		return $components;
	}

	$files = $uploaded_files['images'];
	if(!is_array($files)) {
		$files = [$files];
	}

	foreach($files as $file) {
		if(is_string($file) && is_readable($file)) {
			$components['attachments'][] = $file;
		}
	}

	return $components;
}, 10, 3);

How ever that approach seems unnecessary (not to mention it targets a hard-coded field images as opposed to any file input in any form). With the file input having the unsupported multiple attribute, CF7 still seems to handle attaching all the files to the e-mail correctly.

Reusable code

Here’s the full snippet that can be used to enable multiple file uploading in CF7:

PHP
/**
 * When processing the `[file]` CF7 form tag, add the `ska-multiple` class to mark any file inputs that should have the `multiple` attribute.
 */
add_filter('wpcf7_form_tag', function(array $tag) {
	if(
		$tag['basetype'] === 'file'
		&& isset($tag['options'])
		&& is_array($tag['options'])
		&& in_array('multiple', $tag['options'], true)
	) {
		$tag['options'][] = 'class:ska-multiple';
	}
	return $tag;
});

/**
 * Filter the final CF7 form output.
 * Add `multiple` attribute to any inputs with the `ska-multiple` class name.
 */
add_filter('wpcf7_form_elements', function(string $content) {
	$p = new WP_HTML_Tag_Processor($content);
	while($p->next_tag(['tag_name' => 'input', 'class_name' => 'ska-multiple'])) {
		$p->set_attribute('multiple', 'multiple');
		$p->set_attribute('name', $p->get_attribute('name') . '[]');
		$p->remove_class('ska-multiple');
	}
	return $p->get_updated_html();
});

/**
 * @param object $result
 * @param object $tag
 * @return object
 */
function ska_validate_max_files($result, $tag) {

	if(
		$tag->basetype !== 'file'
		|| !property_exists($tag, 'options')
		|| !is_array($tag->options)
		|| !in_array('multiple', $tag->options, true)
		|| !isset($_FILES[$tag->name])
	) {
		return $result;
	}

	$max = array_find($tag->options, function($v) {
		return is_string($v) && str_starts_with($v, 'maxfiles:') && is_numeric(str_replace('maxfiles:', '', $v));
	});
	if(!is_string($max)) {
		return $result;
	}
	$max = absint(str_replace('maxfiles:', '', $max));

	$files = $_FILES[$tag->name] ?? null; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
	if(!is_array($files) || !isset($files['name'])) {
		return $result;
	}

	$names = is_array($files['name']) ? array_filter($files['name']) : [$files['name']];
	if(count($names) > $max) {

		$message = (
			in_array('errortext', $tag->options, true) 
			&& property_exists($tag, 'labels') 
			&& is_array($tag->labels) 
			&& isset($tag->labels[0]) 
			&& is_string($tag->labels[0])
		) ? sanitize_text_field($tag->labels[0]) : 'Up to %d file attachments are permitted.';

		$result->invalidate($tag, esc_html(sprintf($message, $max)));
	}

	return $result;
}
add_filter('wpcf7_validate_file', 'ska_validate_max_files', 10, 2);
add_filter('wpcf7_validate_file*', 'ska_validate_max_files', 10, 2);

The snippet can be placed in the child theme’s functions.php or a separate file in the child theme’s directory (e.g. cf7.php) and included in functions.php as such:

PHP
require_once get_stylesheet_directory() . '/cf7.php';

If you don’t have a child theme you’ll need to use a mu-plugin or a code snippets plugin (not recommended).

Leave a Reply

Your email address will not be published. Required fields are marked *