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