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 +150 -105 3.1.13.3.1 View file →
@@ -13,8 +13,9 @@
13 13
14 14 use BitCode\BitForm\Core\Integration\IntegrationHandler;
15 15 use BitCode\BitForm\Core\Util\ApiResponse as UtilApiResponse;
16 16 use BitCode\BitForm\Core\Util\HttpHelper;
17 +use BitCode\BitForm\Core\Util\Utilities;
17 18 use BitCode\BitForm\GlobalHelper;
18 19
19 20 /**
20 21 * Provide functionality for webhooks
@@ -44,67 +45,42 @@
44 45 }
45 46
46 47 public static function testWebhook()
47 48 {
48 - if (isset($_REQUEST['_ajax_nonce']) && wp_verify_nonce(sanitize_text_field(wp_unslash($_REQUEST['_ajax_nonce'])), 'bitforms_save')) {
49 - GlobalHelper::requirePostMethod();
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);
51 + }
50 52
51 - try {
52 - $webhookDetails = GlobalHelper::formatRequestData();
53 - } catch (\InvalidArgumentException $e) {
54 - wp_send_json_error($e->getMessage(), 400);
55 - }
53 + GlobalHelper::requirePostMethod();
56 54
57 - $details = is_string($webhookDetails) ? json_decode($webhookDetails)->hookDetails : $webhookDetails->hookDetails;
58 - $method = isset($details->method) ? $details->method : 'get';
59 - $data = isset($details->url) ? WebHooksHandler::urlParserWrapper($details->url) : false;
60 - $response = null;
61 - if ($data) {
62 - $url = $data['url'];
63 - // $url = $details->url;
64 - $params = $data['params'];
65 - $params = IntegrationHandler::replaceFieldWithValue($params, []);
66 - $params['entry_id'] = 'test';
67 - switch (strtoupper($method)) {
68 - case 'GET':
69 - $response = HttpHelper::get($url, $params);
70 - break;
55 + try {
56 + $webhookDetails = GlobalHelper::formatRequestData();
57 + } catch (\InvalidArgumentException $e) {
58 + wp_send_json_error($e->getMessage(), 400);
59 + }
71 60
72 - case 'POST':
73 - $response = HttpHelper::post($url, $params);
74 - break;
61 + $details = is_string($webhookDetails) ? (Utilities::jsonObj($webhookDetails)->hookDetails ?? null) : ($webhookDetails->hookDetails ?? null);
75 62
76 - default:
77 - $response = HttpHelper::request($url, $method, $params);
78 - break;
79 - }
80 - }
81 - if (is_wp_error($response)) {
82 - wp_send_json_error(
83 - empty($response) ? 'Unknown Error Occured' : $response->get_error_message(),
84 - 400
85 - );
86 - }
87 - if (empty($data['url'])) {
88 - wp_send_json_error(__('webhook url is empty', 'bit-form'), 400);
89 - }
90 - wp_send_json_success(['msg' => 'webhook executed succcessfully', 'response' => $response], 200);
91 - } else {
92 - wp_send_json_error(
93 - __(
94 - 'Token expired',
95 - 'bit-form'
96 - ),
97 - 401
98 - );
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);
99 66 }
67 +
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);
100 78 }
101 79
102 80 public function execute(IntegrationHandler $integrationHandler, $integrationDetails, $fieldValues, $entryID, $logID)
103 81 {
104 82 $details = is_string($integrationDetails->integration_details) ? json_decode($integrationDetails->integration_details) : $integrationDetails->integration_details;
105 - $method = isset($details->method) ? $details->method : 'get';
106 - $data = isset($details->url) ? $this->urlParserWrapper($details->url) : false;
107 83
108 84 $entryDetails = [
109 85 'formId' => $this->formID,
110 86 'entryId' => $entryID,
@@ -110,78 +86,81 @@
110 86 'entryId' => $entryID,
111 87 'fieldValues' => $fieldValues
112 88 ];
113 89
114 - if ($data) {
115 - $url = $data['url']; // fix for integromat
116 - // $url = $details->url;
117 - $params = $data['params'];
118 - $params = IntegrationHandler::replaceFieldWithValue($params, $fieldValues);
119 - $params['entry_id'] = $entryID;
120 - switch (strtoupper($method)) {
121 - case 'GET':
122 - $response = HttpHelper::get($url, $params);
123 - break;
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 + }
124 95
125 - case 'POST':
126 - $response = HttpHelper::post($url, $params);
127 - break;
96 + $params = IntegrationHandler::replaceFieldWithValue($data['params'], $fieldValues);
97 + $params['entry_id'] = $entryID;
128 98
129 - default:
130 - $response = HttpHelper::request($url, $method, $params);
131 - break;
132 - }
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);
133 101
134 - if (is_wp_error($response)) {
135 - $this->_logResponse->apiResponse(
136 - $logID,
137 - $this->webhookID,
138 - ['type' => 'record', 'type_name' => 'web hooks'],
139 - 'errors',
140 - $response,
141 - $entryDetails
142 - );
143 - } else {
144 - $this->_logResponse->apiResponse(
145 - $logID,
146 - $this->webhookID,
147 - ['type' => 'record', 'type_name' => 'web hooks'],
148 - 'success',
149 - $response,
150 - $entryDetails
151 - );
152 - }
153 - return $response;
154 - } else {
155 - // if (!class_exists('BitCode\\BitFormPro\\Plugin')) {
156 - // return false;
157 - // }
102 + return $response;
103 + }
158 104
159 - $this->_logResponse->apiResponse(
160 - $logID,
161 - $this->webhookID,
162 - ['type' => 'record', 'type_name' => 'web hooks'],
163 - 'errors',
164 - 'There is something wrong in the webhook url',
165 - $entryDetails
166 - );
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);
167 124 }
168 125 }
169 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 + */
170 145 private static function urlParserWrapper($url)
171 146 {
172 - if (empty($url)) {
173 - return false;
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'));
174 149 }
150 +
175 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 + }
176 155
177 156 $Scheme = isset($parsedURL['scheme']) ? $parsedURL['scheme'] . '://' : null;
178 - $Usr = isset($parsedURL['usr']) ? $parsedURL['usr'] : null;
157 + $Usr = isset($parsedURL['user']) ? $parsedURL['user'] : null;
179 158 $Pass = isset($parsedURL['pass']) ? ':' . $parsedURL['pass'] : null;
180 - $Host = isset($parsedURL['host']) ? $parsedURL['host'] : null;
159 + $Host = $parsedURL['host'];
181 160 $Port = isset($parsedURL['port']) ? ':' . $parsedURL['port'] : null;
182 161 $Path = isset($parsedURL['path']) ? $parsedURL['path'] : null;
183 - $Query = isset($parsedURL['query']) ? $parsedURL['query'] : null;
162 + $Query = isset($parsedURL['query']) ? $parsedURL['query'] : '';
184 163 $Pass = ($Pass || $Usr) ? "$Pass@" : null;
185 164
186 165 $cleanURL = "$Scheme$Usr$Pass$Host$Port$Path";
187 166 $params = [];
@@ -188,9 +167,13 @@
188 167 foreach (explode('&', $Query) as $keyValue) {
189 168 if (empty($keyValue)) {
190 169 continue;
191 170 }
192 - list($field, $value) = explode('=', $keyValue);
171 + $pair = explode('=', $keyValue, 2);
172 + if (2 !== \count($pair)) {
173 + continue;
174 + }
175 + list($field, $value) = $pair;
193 176 if ('' === trim($value)) {
194 177 continue;
195 178 }
196 179 if (isset($params[$field])) {
@@ -204,12 +187,74 @@
204 187 }
205 188 }
206 189
207 190 if (!wp_http_validate_url($cleanURL)) {
208 - return false;
191 + return new \WP_Error('bitform_webhook_url_rejected', self::urlRejectionReason($cleanURL, $parsedURL));
209 192 }
210 193
211 194 return ['url' => $cleanURL, 'params' => $params];
195 + }
196 +
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');
212 + }
213 +
214 + if (isset($parsedURL['user']) || isset($parsedURL['pass'])) {
215 + return __('Webhook url must not contain a username or password.', 'bit-form');
216 + }
217 +
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 + }
222 +
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 + }
227 +
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 + }
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;
212 257 }
213 258
214 259 private function iterate($array)
215 260 {