| @@ -1,8 +1,9 @@ | ||
| 1 | 1 | <?php |
| 2 | 2 | |
| 3 | 3 | namespace FluentForm\App\Services\Integrations\Slack; |
| 4 | 4 | |
| 5 | +use FluentForm\App\Helpers\Helper; | |
| 5 | 6 | use FluentForm\App\Modules\Form\FormDataParser; |
| 6 | 7 | use FluentForm\App\Modules\Form\FormFieldsParser; |
| 7 | 8 | use FluentForm\App\Services\Integrations\LogResponseTrait; |
| 8 | 9 | use FluentForm\Framework\Helpers\ArrayHelper; |
| @@ -24,9 +25,9 @@ | ||
| 24 | 25 | * @param $submissionId |
| 25 | 26 | * @param $formData |
| 26 | 27 | * @param $form |
| 27 | 28 | */ |
| 28 | - public function handle($feed, $formData, $form, $entry) | |
| 29 | + public static function handle($feed, $formData, $form, $entry) | |
| 29 | 30 | { |
| 30 | 31 | $settings = $feed['processedValues']; |
| 31 | 32 | |
| 32 | 33 | $inputs = FormFieldsParser::getEntryInputs($form); |
| @@ -32,35 +33,65 @@ | ||
| 32 | 33 | $inputs = FormFieldsParser::getEntryInputs($form); |
| 33 | 34 | |
| 34 | 35 | $labels = FormFieldsParser::getAdminLabels($form, $inputs); |
| 35 | 36 | |
| 36 | - $formData = FormDataParser::parseData((object)$formData, $inputs, $form->id); | |
| 37 | - | |
| 37 | + $labels = apply_filters_deprecated( | |
| 38 | + 'fluentform_slack_field_label_selection', | |
| 39 | + [ | |
| 40 | + $labels, | |
| 41 | + $settings, | |
| 42 | + $form, | |
| 43 | + ], | |
| 44 | + FLUENTFORM_FRAMEWORK_UPGRADE, | |
| 45 | + 'fluentform/slack_field_label_selection', | |
| 46 | + 'Use fluentform/slack_field_label_selection instead of fluentform_slack_field_label_selection.' | |
| 47 | + ); | |
| 48 | + | |
| 49 | + $labels = apply_filters('fluentform/slack_field_label_selection', $labels, $settings, $form); | |
| 50 | + | |
| 51 | + foreach ($inputs as $name => $input) { | |
| 52 | + if (empty($formData[$name])) { | |
| 53 | + continue; | |
| 54 | + } | |
| 55 | + if ('tabular_grid' == ArrayHelper::get($input, 'element', '')) { | |
| 56 | + $formData[$name] = Helper::getTabularGridFormatValue($formData[$name], $input, '<br />', ', ', 'markdown'); | |
| 57 | + } | |
| 58 | + } | |
| 59 | + $formData = FormDataParser::parseData((object) $formData, $inputs, $form->id); | |
| 60 | + | |
| 38 | 61 | $slackTitle = ArrayHelper::get($settings, 'textTitle'); |
| 39 | - | |
| 40 | - if($slackTitle === '') { | |
| 41 | - $title = "New submission on " . $form->title; | |
| 42 | - }else { | |
| 62 | + | |
| 63 | + if ('' === $slackTitle) { | |
| 64 | + $title = 'New submission on ' . $form->title; | |
| 65 | + } else { | |
| 43 | 66 | $title = $slackTitle; |
| 44 | 67 | } |
| 45 | 68 | |
| 69 | + $footerText = ArrayHelper::get($settings, 'footerText'); | |
| 70 | + if ($footerText === '') { | |
| 71 | + $footerText = 'fluentform'; | |
| 72 | + } | |
| 73 | + | |
| 46 | 74 | $fields = []; |
| 47 | 75 | |
| 48 | 76 | foreach ($formData as $attribute => $value) { |
| 77 | + $value = str_replace('<br />', "\n", $value); | |
| 49 | 78 | $value = str_replace('&', '&', $value); |
| 50 | 79 | $value = str_replace('<', '<', $value); |
| 51 | - $value = str_replace('>', ">", $value); | |
| 52 | - | |
| 80 | + $value = str_replace('>', '>', $value); | |
| 81 | + if (! isset($labels[$attribute]) || empty($value)) { | |
| 82 | + continue; | |
| 83 | + } | |
| 53 | 84 | $fields[] = [ |
| 54 | 85 | 'title' => $labels[$attribute], |
| 55 | 86 | 'value' => $value, |
| 56 | - 'short' => false | |
| 87 | + 'short' => false, | |
| 57 | 88 | ]; |
| 58 | 89 | } |
| 59 | - | |
| 60 | 90 | $slackHook = ArrayHelper::get($settings, 'webhook'); |
| 61 | 91 | |
| 62 | - $titleLink = admin_url('admin.php?page=fluent_forms&form_id=' | |
| 92 | + $titleLink = admin_url( | |
| 93 | + 'admin.php?page=fluent_forms&form_id=' | |
| 63 | 94 | . $form->id |
| 64 | 95 | . '&route=entries#/entries/' |
| 65 | 96 | . $entry->id |
| 66 | 97 | ); |
| @@ -73,16 +104,49 @@ | ||
| 73 | 104 | 'fallback' => $title, |
| 74 | 105 | 'title' => $title, |
| 75 | 106 | 'title_link' => $titleLink, |
| 76 | 107 | 'fields' => $fields, |
| 77 | - 'footer' => 'fluentform', | |
| 78 | - 'ts' => round(microtime(true) * 1000) | |
| 79 | - ] | |
| 80 | - ] | |
| 81 | - ]) | |
| 108 | + 'footer' => $footerText, | |
| 109 | + 'ts' => round(microtime(true) * 1000), | |
| 110 | + ], | |
| 111 | + ], | |
| 112 | + ]), | |
| 82 | 113 | ]; |
| 83 | 114 | |
| 84 | - $result = wp_remote_post($slackHook, [ | |
| 115 | + // SECURITY (FINDING-15): the webhook URL is admin-supplied and was fetched with no egress | |
| 116 | + // restriction — an SSRF with a logged status/error oracle (the response is written into the | |
| 117 | + // feed log). wp_safe_remote_post (below) blocks private/loopback ranges; additionally pin | |
| 118 | + // the host, since Slack incoming webhooks are always https://hooks.slack.com/... , so an | |
| 119 | + // arbitrary external host cannot be used as the oracle target either. | |
| 120 | + // COMPAT: filterable so a site using a Slack-compatible endpoint (Mattermost, Rocket.Chat, | |
| 121 | + // an internal relay) can keep an existing feed working without patching. Site PHP only — | |
| 122 | + // a form manager cannot reach it, so the default-deny posture is preserved. | |
| 123 | + $defaultHookHosts = ['hooks.slack.com']; | |
| 124 | + $allowedHookHosts = (array) apply_filters('fluentform/slack_allowed_webhook_hosts', $defaultHookHosts, $feed); | |
| 125 | + $allowedHookHosts = array_map('strtolower', array_filter($allowedHookHosts, 'is_string')); | |
| 126 | + // A filter returning nothing usable must not silently break every Slack feed — fall back | |
| 127 | + // to the official host so the default keeps working no matter what the filter returns. | |
| 128 | + if (!$allowedHookHosts) { | |
| 129 | + $allowedHookHosts = $defaultHookHosts; | |
| 130 | + } | |
| 131 | + | |
| 132 | + $parsedHook = wp_parse_url($slackHook); | |
| 133 | + $hookHost = isset($parsedHook['host']) ? strtolower($parsedHook['host']) : ''; | |
| 134 | + $hookScheme = isset($parsedHook['scheme']) ? strtolower($parsedHook['scheme']) : ''; | |
| 135 | + if ('https' !== $hookScheme || !in_array($hookHost, $allowedHookHosts, true)) { | |
| 136 | + $message = sprintf( | |
| 137 | + // translators: %s is a comma-separated list of allowed webhook hosts. | |
| 138 | + __('Invalid Slack webhook URL. It must be an https:// URL on: %s', 'fluentform'), | |
| 139 | + implode(', ', $allowedHookHosts) | |
| 140 | + ); | |
| 141 | + do_action('fluentform/integration_action_result', $feed, 'failed', $message); | |
| 142 | + return [ | |
| 143 | + 'status' => 'failed', | |
| 144 | + 'message' => $message, | |
| 145 | + ]; | |
| 146 | + } | |
| 147 | + | |
| 148 | + $result = wp_safe_remote_post($slackHook, [ | |
| 85 | 149 | 'method' => 'POST', |
| 86 | 150 | 'timeout' => 30, |
| 87 | 151 | 'redirection' => 5, |
| 88 | 152 | 'httpversion' => '1.0', |
| @@ -87,9 +151,9 @@ | ||
| 87 | 151 | 'redirection' => 5, |
| 88 | 152 | 'httpversion' => '1.0', |
| 89 | 153 | 'headers' => [], |
| 90 | 154 | 'body' => $body, |
| 91 | - 'cookies' => [] | |
| 155 | + 'cookies' => [], | |
| 92 | 156 | ]); |
| 93 | 157 | |
| 94 | 158 | if (is_wp_error($result)) { |
| 95 | 159 | $status = 'failed'; |
| @@ -95,19 +159,19 @@ | ||
| 95 | 159 | $status = 'failed'; |
| 96 | 160 | $message = $result->get_error_message(); |
| 97 | 161 | } else { |
| 98 | 162 | $message = $result['response']; |
| 99 | - $status = $result['response']['code'] == 200 ? 'success' : 'failed'; | |
| 163 | + $status = 200 == $result['response']['code'] ? 'success' : 'failed'; | |
| 100 | 164 | } |
| 101 | 165 | |
| 102 | - if ($status == 'failed') { | |
| 103 | - do_action('ff_integration_action_result', $feed, 'failed', $message); | |
| 166 | + if ('failed' == $status) { | |
| 167 | + do_action('fluentform/integration_action_result', $feed, 'failed', $message); | |
| 104 | 168 | } else { |
| 105 | - do_action('ff_integration_action_result', $feed, 'success', 'Submission notification has been successfully delivered to slack channel'); | |
| 169 | + do_action('fluentform/integration_action_result', $feed, 'success', 'Submission notification has been successfully delivered to slack channel'); | |
| 106 | 170 | } |
| 107 | 171 | |
| 108 | - return array( | |
| 172 | + return [ | |
| 109 | 173 | 'status' => $status, |
| 110 | - 'message' => $message | |
| 111 | - ); | |
| 174 | + 'message' => $message, | |
| 175 | + ]; | |
| 112 | 176 | } |
| 113 | 177 | } |