`), so they cannot inject links or * `` mentions; the message template itself is the site owner's and is sent as typed. */ final class Slack { public const ID = 'slack'; public const URL_PATTERN = '#^https://hooks\.slack\.com/(?:services|workflows|triggers)/[A-Za-z0-9_\-/]+$#'; /** @return null|WP_Error */ public static function handle(Submission $s, array $settings) { $payload = self::payload($s, $settings); if ($payload instanceof WP_Error) { Log::error(self::ID, $payload->get_error_message(), ['form' => $s->form_id]); return $payload; } $r = Http::json('POST', trim((string) $settings['slack_webhook_url']), $payload); if ($r instanceof WP_Error) { Log::error(self::ID, $r->get_error_message(), ['form' => $s->form_id, 'submission' => $s->id]); return $r; } return null; } public static function escape(string $text): string { return str_replace(['&', '<', '>'], ['&', '<', '>'], $text); } /** @return array|WP_Error */ public static function payload(Submission $s, array $settings) { $get = static fn(string $k): string => isset($settings['slack_' . $k]) && is_scalar($settings['slack_' . $k]) ? trim((string) $settings['slack_' . $k]) : ''; if (!preg_match(self::URL_PATTERN, $get('webhook_url'))) { return new WP_Error('zadex_slack_url', 'Slack: missing or invalid webhook URL.'); } // Render the template against escaped values: the template stays as typed, visitor input is inert. $safe = clone $s; $safe->values = array_map(static fn($v) => is_array($v) ? array_map(static fn($x): string => self::escape((string) $x), $v) : self::escape((string) $v), $s->values); $safe->form_name = self::escape($s->form_name); $template = $get('message'); $text = Shortcodes::render($template !== '' ? $template : 'New submission: *[form:name]*', $safe, 'plain'); $blocks = !array_key_exists('slack_blocks', $settings) || !empty($settings['slack_blocks']); $payload = ['text' => mb_substr($text, 0, 3000)]; if ($blocks) { $out = [['type' => 'header', 'text' => ['type' => 'plain_text', 'text' => mb_substr($s->form_name !== '' ? $s->form_name : 'New submission', 0, 150), 'emoji' => true]]]; if ($text !== '') { $out[] = ['type' => 'section', 'text' => ['type' => 'mrkdwn', 'text' => mb_substr($text, 0, 3000)]]; } $pairs = []; foreach (Fields::inputs($s->fields) as $id => $field) { if ($field['type'] === 'password') { continue; } $value = $s->text($id); $pairs[] = ['type' => 'mrkdwn', 'text' => mb_substr('*' . self::escape($s->label($id)) . "*\n" . ($value !== '' ? self::escape($value) : '—'), 0, 2000)]; } foreach (array_chunk($pairs, 10) as $chunk) { $out[] = ['type' => 'section', 'fields' => $chunk]; } $payload['blocks'] = array_slice($out, 0, 50); } if ($get('username') !== '') { $payload['username'] = mb_substr(Shortcodes::render($get('username'), $s, 'header'), 0, 80); } $icon = $get('icon'); if (preg_match('/^:[a-z0-9_+\-]+:$/i', $icon)) { $payload['icon_emoji'] = $icon; } elseif (preg_match('#^https://\S+$#i', $icon)) { $payload['icon_url'] = esc_url_raw($icon); } return $payload; } }