] one field value * [form:name] the form name * [page:title] title of the page holding the form [page:url] its permalink * [site:name] the site title * * Values are escaped for the context they land in: `html` (esc_html +
), `plain`, `url` * (rawurlencode) or `header` (no CR/LF, used for subjects / names / addresses). */ final class Shortcodes { public const CONTEXTS = ['plain', 'html', 'url', 'header']; public static function render(string $template, Submission $submission, string $context = 'plain'): string { if (!in_array($context, self::CONTEXTS, true)) { $context = 'plain'; } if ($context === 'html') { // Line breaks typed in the settings become
; values are escaped below. $template = nl2br($template, false); } $out = preg_replace_callback( '/\[(all-fields|field:([a-z0-9_\-]+)|form:name|page:title|page:url|site:name)\]/i', static function (array $m) use ($submission, $context): string { $tag = strtolower($m[1]); if ($tag === 'all-fields') { return self::all_fields($submission, $context); } if (str_starts_with($tag, 'field:')) { return self::escape($submission->text(strtolower($m[2])), $context); } switch ($tag) { case 'form:name': return self::escape($submission->form_name, $context); case 'page:title': return self::escape(function_exists('get_the_title') ? wp_strip_all_tags((string) get_the_title($submission->post_id)) : '', $context); case 'page:url': return self::escape(function_exists('get_permalink') ? (string) get_permalink($submission->post_id) : '', $context); case 'site:name': return self::escape(function_exists('get_bloginfo') ? wp_strip_all_tags((string) get_bloginfo('name')) : '', $context); } return ''; }, $template ); // Headers: the typed template itself must not smuggle line breaks either (subject / name / address). return $context === 'header' ? self::header_safe((string) $out) : (string) $out; } public static function escape(string $value, string $context): string { switch ($context) { case 'html': return nl2br(esc_html($value), false); case 'url': return rawurlencode($value); case 'header': return self::header_safe($value); default: return $value; } } /** Strips CR/LF/NUL (mail header injection) and collapses whitespace. */ public static function header_safe(string $value): string { $value = preg_replace('/[\r\n\x00]+/', ' ', $value) ?? ''; return trim(preg_replace('/\s{2,}/', ' ', $value) ?? ''); } private static function all_fields(Submission $submission, string $context): string { $lines = []; foreach (Fields::inputs($submission->fields) as $id => $field) { if ($field['type'] === 'password') { continue; } $label = $submission->label($id); $value = $submission->text($id); if ($context === 'html') { if ($field['type'] === 'upload' && is_array($submission->values[$id] ?? null)) { $links = array_map(static fn($u): string => '' . esc_html(basename((string) $u)) . '', $submission->values[$id]); $value_html = implode('
', $links); } else { $value_html = nl2br(esc_html($value), false); } $lines[] = '

' . esc_html($label) . '
' . ($value_html !== '' ? $value_html : '—') . '

'; } else { $lines[] = $label . ': ' . $value; } } if ($context === 'html') { return implode('', $lines); } $text = implode("\n", $lines); return $context === 'plain' ? $text : self::escape($text, $context); } }