PluginProbe
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder / 3.3.1
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder v3.3.1
3.3.1 V-3.3.0 3.2.2 3.2.1 3.2.0 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 V3.0.3 V3.0.2 -3.0.1 V_3.0.0 1.1.1 1.1.8 1.2 1.3 1.4 1.4.18 1.5.2 1.9 2.0 2.10.0 2.10.1 All 138 releases
← All changes | includes/Core/Integration/WebHooks/WebHooksHandler.php +233 -114 1.33.3.1 View file →
@@ -6,10 +6,17 @@
6 6 */
7 7
8 8 namespace BitCode\BitForm\Core\Integration\WebHooks;
9 9
10 +if (!defined('ABSPATH')) {
11 + exit;
12 +}
13 +
14 +use BitCode\BitForm\Core\Integration\IntegrationHandler;
15 +use BitCode\BitForm\Core\Util\ApiResponse as UtilApiResponse;
10 16 use BitCode\BitForm\Core\Util\HttpHelper;
11 -use BitCode\BitForm\Core\Integration\IntegrationHandler;
17 +use BitCode\BitForm\Core\Util\Utilities;
18 +use BitCode\BitForm\GlobalHelper;
12 19
13 20 /**
14 21 * Provide functionality for webhooks
15 22 */
@@ -14,137 +21,249 @@
14 21 * Provide functionality for webhooks
15 22 */
16 23 class WebHooksHandler
17 24 {
18 - private $fromID;
19 - private $webhookID;
25 + private $formID;
26 + private $webhookID;
20 27
21 - public function __construct($webhookID, $fromID)
22 - {
23 - $this->formID = $fromID;
24 - $this->webhookID = $webhookID;
28 + private $_logResponse;
29 +
30 + public function __construct($webhookID, $formID)
31 + {
32 + $this->formID = $formID;
33 + $this->webhookID = $webhookID;
34 + $this->_logResponse = new UtilApiResponse();
35 + }
36 +
37 + /**
38 + * Helps to register ajax function's with wp
39 + *
40 + * @return null
41 + */
42 + public static function registerAjax()
43 + {
44 + add_action('wp_ajax_bitforms_test_webhook', [__CLASS__, 'testWebhook']);
45 + }
46 +
47 + public static function testWebhook()
48 + {
49 + if (!isset($_REQUEST['_ajax_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_REQUEST['_ajax_nonce'])), 'bitforms_save')) {
50 + wp_send_json_error(__('Token expired', 'bit-form'), 401);
25 51 }
26 52
27 - /**
28 - * Helps to register ajax function's with wp
29 - *
30 - * @return null
31 - */
32 - public static function registerAjax()
33 - {
34 - add_action('wp_ajax_bitforms_test_webhook', array(__CLASS__, 'testWebhook'));
53 + GlobalHelper::requirePostMethod();
54 +
55 + try {
56 + $webhookDetails = GlobalHelper::formatRequestData();
57 + } catch (\InvalidArgumentException $e) {
58 + wp_send_json_error($e->getMessage(), 400);
35 59 }
36 60
37 - public static function testWebhook()
38 - {
39 - if (isset($_REQUEST['_ajax_nonce']) && wp_verify_nonce(sanitize_text_field($_REQUEST['_ajax_nonce']), 'bitforms_save')) {
40 - $inputJSON = file_get_contents('php://input');
41 - $webhookDetails = json_decode($inputJSON);
42 - $details = is_string($webhookDetails) ? json_decode($webhookDetails)->hookDetails : $webhookDetails->hookDetails;
43 - $method = isset($details->method) ? $details->method : 'get';
44 - $data = isset($details->url) ? WebHooksHandler::urlParserWrapper($details->url) : false;
45 - $response = null;
46 - if ($data) {
47 - $url = $data['url'];
48 - $params = $data['params'];
49 - $params['entry_id'] = 'test';
50 - switch (strtoupper($method)) {
51 - case 'GET':
52 - $response = HttpHelper::get($url, $params);
53 - break;
61 + $details = is_string($webhookDetails) ? (Utilities::jsonObj($webhookDetails)->hookDetails ?? null) : ($webhookDetails->hookDetails ?? null);
54 62
55 - case 'POST':
56 - $response = HttpHelper::post($url, $params);
57 - break;
63 + $data = self::urlParserWrapper(isset($details->url) ? $details->url : '');
64 + if (is_wp_error($data)) {
65 + wp_send_json_error($data->get_error_message(), 400);
66 + }
58 67
59 - default:
60 - $response = HttpHelper::request($url, $method, $params);
61 - break;
62 - }
63 - }
64 - if (is_wp_error($response)) {
65 - wp_send_json_error(
66 - empty($response) ? 'Unknown Error Occured' : $response->get_error_message(),
67 - 400
68 - );
69 - }
70 - if (empty($data['url'])) {
71 - wp_send_json_error(__("webhook url is empty", "bitform"), 400);
72 - }
73 - wp_send_json_success(__("webhook executed succcessfully", "bitform"), 200);
68 + $params = IntegrationHandler::replaceFieldWithValue($data['params'], []);
69 + $params['entry_id'] = 'test';
70 +
71 + $response = self::sendRequest($data['url'], isset($details->method) ? $details->method : 'get', $params);
72 + if (is_wp_error($response)) {
73 + $errorMessage = $response->get_error_message();
74 + wp_send_json_error('' === $errorMessage ? __('Unknown error occurred', 'bit-form') : $errorMessage, 400);
75 + }
76 +
77 + wp_send_json_success(['msg' => 'webhook executed succcessfully', 'response' => $response], 200);
78 + }
79 +
80 + public function execute(IntegrationHandler $integrationHandler, $integrationDetails, $fieldValues, $entryID, $logID)
81 + {
82 + $details = is_string($integrationDetails->integration_details) ? json_decode($integrationDetails->integration_details) : $integrationDetails->integration_details;
83 +
84 + $entryDetails = [
85 + 'formId' => $this->formID,
86 + 'entryId' => $entryID,
87 + 'fieldValues' => $fieldValues
88 + ];
89 +
90 + $data = self::urlParserWrapper(isset($details->url) ? $details->url : '');
91 + if (is_wp_error($data)) {
92 + $this->logWebhookResponse($logID, 'errors', $data->get_error_message(), $entryDetails);
93 + return false;
94 + }
95 +
96 + $params = IntegrationHandler::replaceFieldWithValue($data['params'], $fieldValues);
97 + $params['entry_id'] = $entryID;
98 +
99 + $response = self::sendRequest($data['url'], isset($details->method) ? $details->method : 'get', $params);
100 + $this->logWebhookResponse($logID, is_wp_error($response) ? 'errors' : 'success', $response, $entryDetails);
101 +
102 + return $response;
103 + }
104 +
105 + /**
106 + * Dispatches the webhook request with the configured http method.
107 + *
108 + * @param string $url
109 + * @param string $method
110 + * @param array $params
111 + * @return mixed|\WP_Error
112 + */
113 + private static function sendRequest($url, $method, $params)
114 + {
115 + switch (strtoupper($method)) {
116 + case 'GET':
117 + return HttpHelper::get($url, $params);
118 +
119 + case 'POST':
120 + return HttpHelper::post($url, $params);
121 +
122 + default:
123 + return HttpHelper::request($url, $method, $params);
124 + }
125 + }
126 +
127 + private function logWebhookResponse($logID, $status, $response, $entryDetails)
128 + {
129 + $this->_logResponse->apiResponse(
130 + $logID,
131 + $this->webhookID,
132 + ['type' => 'record', 'type_name' => 'web hooks'],
133 + $status,
134 + $response,
135 + $entryDetails
136 + );
137 + }
138 +
139 + /**
140 + * Splits a webhook url into the url to call and its query params.
141 + *
142 + * @param mixed $url url as it comes from the saved integration details
143 + * @return array|\WP_Error ['url' => string, 'params' => array], or why the url was rejected
144 + */
145 + private static function urlParserWrapper($url)
146 + {
147 + if (!is_string($url) || '' === trim($url)) {
148 + return new \WP_Error('bitform_webhook_url_empty', __('Webhook url is empty. Please add a url and try again.', 'bit-form'));
149 + }
150 +
151 + $parsedURL = wp_parse_url($url);
152 + if (empty($parsedURL['host'])) {
153 + return new \WP_Error('bitform_webhook_url_invalid', __('Webhook url is not a valid url.', 'bit-form'));
154 + }
155 +
156 + $Scheme = isset($parsedURL['scheme']) ? $parsedURL['scheme'] . '://' : null;
157 + $Usr = isset($parsedURL['user']) ? $parsedURL['user'] : null;
158 + $Pass = isset($parsedURL['pass']) ? ':' . $parsedURL['pass'] : null;
159 + $Host = $parsedURL['host'];
160 + $Port = isset($parsedURL['port']) ? ':' . $parsedURL['port'] : null;
161 + $Path = isset($parsedURL['path']) ? $parsedURL['path'] : null;
162 + $Query = isset($parsedURL['query']) ? $parsedURL['query'] : '';
163 + $Pass = ($Pass || $Usr) ? "$Pass@" : null;
164 +
165 + $cleanURL = "$Scheme$Usr$Pass$Host$Port$Path";
166 + $params = [];
167 + foreach (explode('&', $Query) as $keyValue) {
168 + if (empty($keyValue)) {
169 + continue;
170 + }
171 + $pair = explode('=', $keyValue, 2);
172 + if (2 !== \count($pair)) {
173 + continue;
174 + }
175 + list($field, $value) = $pair;
176 + if ('' === trim($value)) {
177 + continue;
178 + }
179 + if (isset($params[$field])) {
180 + if (\is_array($params[$field])) {
181 + $params[$field][] = sanitize_text_field(urldecode($value));
74 182 } else {
75 - wp_send_json_error(
76 - __(
77 - 'Token expired',
78 - 'bitform'
79 - ),
80 - 401
81 - );
183 + $params[$field] = [$params[$field], sanitize_text_field(urldecode($value))];
82 184 }
185 + } else {
186 + $params[$field] = sanitize_text_field(urldecode($value));
187 + }
83 188 }
84 189
85 - public function execute(IntegrationHandler $integrationHandler, $integrationDetails, $fieldValues, $entryID, $logID)
86 - {
87 - $details = is_string($integrationDetails->integration_details) ? json_decode($integrationDetails->integration_details) : $integrationDetails->integration_details;
88 - $method = isset($details->method) ? $details->method : 'get';
89 - $data = isset($details->url) ? $this->urlParserWrapper($details->url) : false;
90 - if ($data) {
91 - $url = $data['url'];
92 - $params = $data['params'];
93 - $params = IntegrationHandler::replaceFieldWithValue($params, $fieldValues);
94 - $params['entry_id'] = $entryID;
95 - switch (strtoupper($method)) {
96 - case 'GET':
97 - $response = HttpHelper::get($url, $params);
98 - break;
190 + if (!wp_http_validate_url($cleanURL)) {
191 + return new \WP_Error('bitform_webhook_url_rejected', self::urlRejectionReason($cleanURL, $parsedURL));
192 + }
99 193
100 - case 'POST':
101 - $response = HttpHelper::post($url, $params);
102 - break;
194 + return ['url' => $cleanURL, 'params' => $params];
195 + }
103 196
104 - default:
105 - $response = HttpHelper::request($url, $method, $params);
106 - break;
107 - }
108 - }
197 + /**
198 + * Explains why WordPress refused the url, so the message points at the real cause
199 + * (internal host, unresolvable dns, blocked port) instead of "url is empty".
200 + *
201 + * @param string $url url as it was handed to wp_http_validate_url()
202 + * @param array $parsedURL wp_parse_url() output of the original url
203 + * @return string
204 + */
205 + private static function urlRejectionReason($url, $parsedURL)
206 + {
207 + $host = isset($parsedURL['host']) ? trim($parsedURL['host'], '.') : '';
208 + $scheme = isset($parsedURL['scheme']) ? strtolower($parsedURL['scheme']) : '';
209 +
210 + if ('http' !== $scheme && 'https' !== $scheme) {
211 + return __('Webhook url must start with http:// or https://.', 'bit-form');
109 212 }
110 213
111 - private static function urlParserWrapper($url)
112 - {
113 - if (empty($url)) {
114 - return false;
115 - }
116 - $parsedURL = wp_parse_url($url);
214 + if (isset($parsedURL['user']) || isset($parsedURL['pass'])) {
215 + return __('Webhook url must not contain a username or password.', 'bit-form');
216 + }
117 217
118 - $Scheme = isset($parsedURL['scheme']) ? $parsedURL['scheme'] . '://' : null;
119 - $Usr = isset($parsedURL['usr']) ? $parsedURL['usr'] : null;
120 - $Pass = isset($parsedURL['pass']) ? ':' . $parsedURL['pass'] : null;
121 - $Host = isset($parsedURL['host']) ? $parsedURL['host'] : null;
122 - $Port = isset($parsedURL['port']) ? ':' . $parsedURL['port'] : null;
123 - $Path = isset($parsedURL['path']) ? $parsedURL['path'] : null;
124 - $Query = isset($parsedURL['query']) ? $parsedURL['query'] : null;
125 - $Pass = ($Pass || $Usr) ? "$Pass@" : null;
218 + if (!filter_var($host, FILTER_VALIDATE_IP) && gethostbyname($host) === $host) {
219 + /* translators: %s: webhook host name */
220 + return sprintf(__('The host "%s" could not be resolved from this server. Check the url spelling and the server DNS.', 'bit-form'), $host);
221 + }
126 222
127 - $cleanURL = "$Scheme$Usr$Pass$Host$Port$Path";
128 - $params = array();
129 - foreach (explode('&', $Query) as $keyValue) {
130 - if (empty($keyValue)) {
131 - continue;
132 - }
133 - list($field, $value) = explode('=', $keyValue);
134 - if ('' == trim($value)) {
135 - continue;
136 - }
137 - if (isset($params[$field])) {
138 - if (\is_array($params[$field])) {
139 - $params[$field][] = sanitize_text_field(urldecode($value));
140 - } else {
141 - $params[$field] = [$params[$field], sanitize_text_field(urldecode($value))];
142 - }
143 - } else {
144 - $params[$field] = sanitize_text_field(urldecode($value));
145 - }
146 - }
223 + if (self::validatesAsExternalHost($url)) {
224 + /* translators: %s: webhook host name */
225 + return sprintf(__('The host "%s" resolves to a private or local IP address. WordPress blocks requests to internal hosts, allow it with the "http_request_host_is_external" filter.', 'bit-form'), $host);
226 + }
147 227
148 - return array('url' => $cleanURL, 'params' => $params);
228 + if (!empty($parsedURL['port'])) {
229 + $allowedPorts = apply_filters('http_allowed_safe_ports', [80, 443, 8080], $host, $url);
230 + if (\is_array($allowedPorts) && !\in_array((int) $parsedURL['port'], $allowedPorts, true)) {
231 + /* translators: 1: port number, 2: comma separated list of allowed ports */
232 + return sprintf(__('Port %1$d is not allowed for outgoing requests. WordPress only allows %2$s, extend it with the "http_allowed_safe_ports" filter.', 'bit-form'), (int) $parsedURL['port'], implode(', ', $allowedPorts));
233 + }
149 234 }
235 +
236 + return __('Webhook url is not a valid url.', 'bit-form');
237 + }
238 +
239 + /**
240 + * Re-checks the url while treating the host as external, which tells the local/private
241 + * IP rejection apart from every other reason wp_http_validate_url() can fail.
242 + *
243 + * @param string $url
244 + * @return bool
245 + */
246 + private static function validatesAsExternalHost($url)
247 + {
248 + $allowExternal = function () {
249 + return true;
250 + };
251 +
252 + add_filter('http_request_host_is_external', $allowExternal, 99);
253 + $isValid = (bool) wp_http_validate_url($url);
254 + remove_filter('http_request_host_is_external', $allowExternal, 99);
255 +
256 + return $isValid;
257 + }
258 +
259 + private function iterate($array)
260 + {
261 + $ar = [];
262 + if (is_array($array)) {
263 + foreach ($array as $k => $v) {
264 + $ar[$k] = str_replace("\'", "'", $v);
265 + }
266 + }
267 + return $ar;
268 + }
150 269 }