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 = ''; return $list; } public static function convert_to_file_table($data, $field) { if (!$data) { return ''; } $table = '
'; if (\is_array($data) && !isset($data['url'])) { foreach ($data as $file) { $table .= '' . \esc_html((string)($file['name'] ?? '')) . ''; } } else { $table .= '' . \esc_html((string)($data['name'] ?? '')) . ''; } $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 .= ''; if (!is_array($field)) { $table .= ''; } 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('', implode('', $innerTables)); } else if (($field['type'] ?? '') == 'file') { $table .= sprintf('', self::convert_to_file_table($field['value'], $field)); } else { $table .= sprintf('', \implode(', ', \array_map(function ($value) { return \esc_html((string)(\is_array($value) ? \implode(', ', $value) : $value)); }, (array)$field['value_label']))); } } else { $table .= sprintf('', \esc_html((string)$field['value_label'])); } $table .= ''; } $table .= '
' . \esc_html((string)(!is_array($field) ? $key : ($field['label'] ?? $key))) . ':' . \esc_html((string)$field) . '%s%s%s%s
'; 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; } }