false]);
$template = $twig->createTemplate($string);
return $template->render($parameters);
}
/**
* Create a html table from a multi dimensional array of rows and cells
*
* @param array $data
*
* @return string
*/
public static function to_html($data = array())
{
$rows = array();
foreach ($data as $row) {
$cells = array();
if (!is_array($row)) {
continue;
}
foreach ($row as $cell) {
if (is_array($cell)) {
$cell = implode(', ', $cell);
}
$cells[] = "
{$cell} | ";
}
$rows[] = "" . implode('', $cells) . '
';
}
return "" . implode('', $rows) . '
';
}
/**
* Create an html table from a multi dimensional array of rows and cells
*
* @date 2024-05-23
*
* @param array $data
* @param Form $form
*
* @return string
*/
public static function create_table($data, Form $form)
{
$output = [];
foreach ($data as $field) {
if ($field['type'] == 'repeater_start') {
$innerTables = array_map(function ($item) {
return self::convert_to_fields_table($item);
}, $field['value_label']);
$output[] = [
\esc_html((string)($field['label'] ?? '')),
implode('', $innerTables)
];
continue;
}
$value = $field['value_label'] ?? $field['value'] ?? '';
$output[] = [
\esc_html((string)($field['label'] ?? '')),
\is_array($value) ? \array_map(function ($item) {
return \esc_html((string)(\is_array($item) ? \implode(', ', $item) : $item));
}, $value) : \esc_html((string)$value)
];
}
return Helper::to_html($output);
}
public static function is_list(array $arr)
{
if ($arr === []) {
return true;
}
return array_keys($arr) === range(0, count($arr) - 1);
}
public static function create_tree_list($array, $class = "af-tree")
{
$list = '';
foreach ($array as $key => $value) {
if (is_array($value)) {
$is_list = self::is_list($value);
$list .= \sprintf('- %s: %s...%s%s
', $key, $is_list ? "[" : "{", self::create_tree_list($value, ""), $is_list ? "]" : "}");
} else {
$list .= \sprintf('- %s: %s
', $key, htmlentities($value));
}
}
$list .= '
';
return $list;
}
public static function convert_to_file_table($data, $field)
{
if (!$data) {
return '';
}
$table = '';
return $table;
}
public static function convert_to_fields_table($data)
{
if (!$data) {
return '';
}
// Everything below comes from whoever filled the form in, so it is
// escaped on the way out. The nested tables are built here and are
// already escaped, which is why they alone go out as markup.
$table = '';
foreach ($data as $key => $field) {
$table .= '';
$table .= '| ' . \esc_html((string)(!is_array($field) ? $key : ($field['label'] ?? $key))) . ': | ';
if (!is_array($field)) {
$table .= '' . \esc_html((string)$field) . ' | ';
} elseif (is_object($field['value_label']) || is_array($field['value_label'])) {
if (($field['type'] ?? '') == 'repeater_start') {
$innerTables = array_map(function ($item) {
return self::convert_to_fields_table($item);
}, (array)$field['value_label']);
$table .= sprintf('%s | ', implode('', $innerTables));
} else if (($field['type'] ?? '') == 'file') {
$table .= sprintf('%s | ', self::convert_to_file_table($field['value'], $field));
} else {
$table .= sprintf('%s | ', \implode(', ', \array_map(function ($value) {
return \esc_html((string)(\is_array($value) ? \implode(', ', $value) : $value));
}, (array)$field['value_label'])));
}
} else {
$table .= sprintf('%s | ', \esc_html((string)$field['value_label']));
}
$table .= '
';
}
$table .= '
';
return $table;
}
public static function get_repeater_fields($name, Form $form, $exclude, $exclude_types)
{
$fields = $form->get_fields();
$start_repeater = false;
$repeater_fields = [];
foreach ($fields as $field) {
if ((in_array($field['type'], $exclude_types) || in_array($field['name'], $exclude)) && !in_array($field['type'], ['repeater_start', 'repeater_end'])) {
continue;
}
if ($field['type'] === 'repeater_start' && $field['name'] == $name) {
$start_repeater = true;
continue;
}
if ($start_repeater) {
if ($field['type'] == 'repeater_end') {
$start_repeater = false;
return $repeater_fields;
}
$repeater_fields[] = $field;
}
}
return $repeater_fields;
}
public static function get_field_value_label($field, $data)
{
$value = isset($data[$field['name']]) ? $data[$field['name']] : $field['default'];
switch ($field['type']) {
// case 'file':
// if (\is_array($data[$field['name']]) && !isset($data[$field['name']]['url'])) {
// $values = [];
// foreach ($data[$field['name']] as $file) {
// $values[] = [
// $file['url'] ?? ''
// ];
// }
// return $values;
// } else {
// return $data[$field['name']]['url'] ?? '';
// }
case 'posts':
if (\is_array($data[$field['name']])) {
$values = [];
foreach ($data[$field['name']] as $post_id) {
$pst = get_post($post_id);
$values[] = $pst && !is_wp_error($pst) ? $pst->post_title : '';
}
return $values;
} else {
$pst = get_post($data[$field['name']]);
return $pst && !\is_wp_error($pst) ? $pst->post_title : '';
}
case 'terms':
if (\is_array($data[$field['name']])) {
$values = [];
foreach ($data[$field['name']] as $term_id) {
$trm = get_term($term_id, $field['taxonomy']);
$values[] = $trm && !\is_wp_error($trm) ? $trm->name : '';
}
return $values;
} else {
$trm = get_term($data[$field['name']], $field['taxonomy']);
return $trm && !\is_wp_error($trm) ? $trm->name : '';
}
case 'term_posts':
$values = [];
if (isset($data[$field['name']]['terms'])) {
$trm = get_term($data[$field['name']]['terms'], $field['taxonomy']);
$values[] = $trm && !\is_wp_error($trm) ? $trm->name : '';
}
if (isset($data[$field['name']]['posts'])) {
$pst = get_post($data[$field['name']]['posts']);
$values[] = $pst && !\is_wp_error($pst) ? $pst->post_title : '';
}
return \implode(' - ', $values);
default:
return $value;
}
}
/**
* Strip anything unsafe out of a submitted value
*
* Backs the "Sanitize Html" field setting, which until now was offered in
* the builder but never applied to anything.
*
* @date 2026-10-18
*
* @param mixed $value
*
* @return mixed
*/
public static function sanitize_html($value)
{
if (\is_array($value)) {
return \array_map([self::class, 'sanitize_html'], $value);
}
if (!\is_string($value) || '' === $value) {
return $value;
}
return \wp_kses_post($value);
}
public static function parse_submit_data($data, Form $form, $exclude = ['_message', '_token'], $exclude_types = ['submit', 'button', 'recaptcha', 'turnstile', 'html', 'repeater_end'])
{
$nData = [];
$fields = $form->get_fields();
foreach ($fields as $field) {
if (in_array($field['type'], $exclude_types) || in_array($field['name'], $exclude)) {
continue;
}
$value = isset($data[$field['name']]) ? $data[$field['name']] : $field['default'] ?? '';
$label = $field['label'] && !empty($field['label']) ? $field['label'] : null;
$nData[$field['name']] = [
'type' => $field['type'],
'value' => $value,
'value_label' => $value,
'label' => $label ? $label : $field['attr']['placeholder'] ?? '',
];
if ($field['type'] == 'repeater_start') {
$repeater_fields = self::get_repeater_fields($field['name'], $form, $exclude, $exclude_types);
$indexes = [];
foreach ($data[$field['name']] as $key => $value) {
$index = (int)preg_replace('/.*--(\d+)$/', '$1', $key);
$indexes[(string)$index] = $key;
}
$keys = array_keys($indexes);
$values_label = [];
foreach ($keys as $key) {
$innerValues = [];
foreach ($repeater_fields as $repeater_field) {
$exclude[] = $repeater_field['name'];
$repeater_field['name'] = $repeater_field['name'] . '--' . $key;
$innerValues[] = [
'label' => $repeater_field['label'],
'value' => isset($data[$field['name']][$repeater_field['name']]) ? $data[$field['name']][$repeater_field['name']] : $repeater_field['default'],
'value_label' => self::get_field_value_label($repeater_field, $data[$field['name']])
];
}
$values_label[] = $innerValues;
}
$nData[$field['name']]['value_label'] = $values_label;
continue;
}
if (!$value) {
continue;
}
$nData[$field['name']]['value_label'] = self::get_field_value_label($field, $data);
}
return $nData;
}
public static function parse_file_field_options($field_options, $validation = true)
{
$mime_types = [];
if (isset($field_options['mime_types']) && !empty($field_options['mime_types'])) {
$mime_types = \array_map(function ($mime_type) {
return trim($mime_type['value']);
}, $field_options['mime_types']);
} else {
$mime_types = ['application/pdf', 'image/jpeg', 'image/png', 'image/gif'];
}
$field_options = array_filter($field_options, function ($key) {
return !in_array($key, ['max_size', 'max_size_message', 'extensions', 'extensions_message', 'not_found_message', 'not_readable_message', 'upload_cant_write_error_message', 'upload_error_message', 'upload_form_size_error_message', 'upload_ini_size_error_message', 'upload_no_file_error_message', 'upload_partial_error_message', 'mime_types', 'mime_types_message']);
}, ARRAY_FILTER_USE_KEY);
$multiple = $field_options['multiple'] ?? false;
if ($validation) {
$constraint = new File(
[],
$field_options['max_size'] ? $field_options['max_size'] : null,
null,
$mime_types,
$field_options['not_found_message'] ?? null,
$field_options['not_readable_message'] ?? null,
$field_options['max_size_message'] ?? null,
$field_options['mime_types_message'] ?? null,
null,
$field_options['upload_ini_size_error_message'] ?? null,
$field_options['upload_form_size_error_message'] ?? null,
$field_options['upload_partial_error_message'] ?? null,
$field_options['upload_no_file_error_message'] ?? null,
null,
$field_options['upload_cant_write_error_message'] ?? null,
null,
$field_options['upload_error_message'] ?? null
);
if ($multiple) {
$field_options['constraints'][] = $constraint;
$field_options['constraints'] = new All($field_options['constraints']);
} else {
$field_options['constraints'] = $constraint;
}
} else {
unset($field_options['constraints']);
unset($field_options['required']);
}
return $field_options;
}
/**
* Extensions that are never accepted, whatever the field allows
*/
const BLOCKED_UPLOAD_EXTENSIONS = [
'php', 'php3', 'php4', 'php5', 'php7', 'php8', 'phps', 'pht', 'phtm', 'phtml', 'phar',
'shtml', 'htaccess', 'htpasswd', 'cgi', 'pl', 'py', 'rb', 'sh', 'bash', 'exe', 'dll',
'jsp', 'asp', 'aspx', 'swf', 'svgz',
];
public static function validate_upload($form, $file, $extensions, $extensions_message, $invalid_message, $required = true)
{
if ($file instanceof \Symfony\Component\HttpFoundation\File\UploadedFile) {
$extension = \strtolower($file->getClientOriginalExtension());
if ('' === $extension || \in_array($extension, self::BLOCKED_UPLOAD_EXTENSIONS, true)) {
$form->addError(new FormError($invalid_message));
return;
}
if ($extensions && \count($extensions) > 0) {
if (!in_array(strtolower($file->getClientOriginalExtension()), $extensions)) {
$message = \str_replace(['{{ extension }}', '{{ extensions }}'], [$file->getClientOriginalExtension(), implode(', ', $extensions)], $extensions_message ? $extensions_message : 'The extension of the file is invalid ({{ extension }}). Allowed extensions are {{ extensions }}');
$form->addError(new FormError($message));
}
}
} else if ($required) {
$form->addError(new FormError($invalid_message));
}
}
public static function create_file_field($builder, $field, $field_options, $validation = true)
{
$field_options = self::parse_file_field_options($field_options, $validation);
$extensions = $field_options['extensions'] ?? null;
$extensions_message = $field_options['extensions_message'] ?? null;
$multiple = $field_options['multiple'] ?? false;
$required = $field_options['required'] ?? false;
$invalid_message = $field_options['invalid_message'] ?? __('The file is invalid.', 'ajaxy-forms');
$builder->add($field['name'], FileType::class, $field_options);
if ($multiple) {
$builder->get($field['name'])->addModelTransformer(new UploadedFilesTransformer());
$builder->get($field['name'])->addEventListener(FormEvents::POST_SUBMIT, function ($event) use ($extensions, $extensions_message, $invalid_message, $required) {
$form = $event->getForm();
$data = $form->getData();
if ($extensions) {
$extensions = \array_map(function ($ext) {
return trim(strtolower($ext['value']));
}, (array)$extensions);
}
if ($data && !empty($data)) {
foreach ($data as $file) {
self::validate_upload($form, $file, $extensions, $extensions_message, $invalid_message, $required);
}
} else if ($required) {
$form->addError(new FormError($invalid_message));
}
});
} else {
$builder->get($field['name'])->addModelTransformer(new UploadedFileTransformer());
$builder->get($field['name'])->addEventListener(FormEvents::POST_SUBMIT, function ($event) use ($extensions, $extensions_message, $invalid_message, $required) {
$form = $event->getForm();
$data = $form->getData();
self::validate_upload($form, $data, $extensions, $extensions_message, $invalid_message, $required);
});
}
}
public static function handle_files(&$data)
{
if (!function_exists('wp_handle_upload')) {
require_once(ABSPATH . 'wp-admin/includes/file.php');
require_once(ABSPATH . 'wp-admin/includes/image.php');
require_once(ABSPATH . 'wp-admin/includes/media.php');
}
foreach ($data as $key => &$val) {
if ($val instanceof \Symfony\Component\HttpFoundation\File\UploadedFile) {
$file = array(
'name' => $val->getClientOriginalName(),
'type' => 'file',
'mime' => $val->getClientMimeType(),
'tmp_name' => $val->getPathname(),
'error' => $val->getError(),
'size' => $val->getSize()
);
// test_type is what stops a .php landing in the uploads
// folder, it stays on. A form that needs a type WordPress
// does not allow should widen it through upload_mimes.
$uploaded_file = wp_handle_upload($file, ['test_form' => false, 'test_size' => true, 'test_type' => true], null);
if (!isset($uploaded_file['error'])) {
unset($file['tmp_name']);
unset($file['error']);
$file['url'] = $uploaded_file['url'];
$data[$key] = $file;
} else {
\error_log($uploaded_file['error']);
}
} else if (\is_array($val)) {
$data[$key] = self::handle_files($val);
}
}
return $data;
}
}