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 +184 -90 2.03.3.1 View file →
@@ -6,21 +6,31 @@
6 6 */
7 7
8 8 namespace BitCode\BitForm\Core\Integration\WebHooks;
9 9
10 +if (!defined('ABSPATH')) {
11 + exit;
12 +}
13 +
10 14 use BitCode\BitForm\Core\Integration\IntegrationHandler;
11 15 use BitCode\BitForm\Core\Util\ApiResponse as UtilApiResponse;
12 16 use BitCode\BitForm\Core\Util\HttpHelper;
17 +use BitCode\BitForm\Core\Util\Utilities;
18 +use BitCode\BitForm\GlobalHelper;
13 19
14 20 /**
15 21 * Provide functionality for webhooks
16 22 */
17 -class WebHooksHandler {
18 - private $fromID;
23 +class WebHooksHandler
24 +{
25 + private $formID;
19 26 private $webhookID;
20 27
21 - public function __construct($webhookID, $fromID) {
22 - $this->formID = $fromID;
28 + private $_logResponse;
29 +
30 + public function __construct($webhookID, $formID)
31 + {
32 + $this->formID = $formID;
23 33 $this->webhookID = $webhookID;
24 34 $this->_logResponse = new UtilApiResponse();
25 35 }
26 36
@@ -28,116 +38,129 @@
28 38 * Helps to register ajax function's with wp
29 39 *
30 40 * @return null
31 41 */
32 - public static function registerAjax() {
42 + public static function registerAjax()
43 + {
33 44 add_action('wp_ajax_bitforms_test_webhook', [__CLASS__, 'testWebhook']);
34 45 }
35 46
36 - public static function testWebhook() {
37 - if (isset($_REQUEST['_ajax_nonce']) && wp_verify_nonce(sanitize_text_field($_REQUEST['_ajax_nonce']), 'bitforms_save')) {
38 - $inputJSON = file_get_contents('php://input');
39 - $webhookDetails = json_decode($inputJSON);
40 - $details = is_string($webhookDetails) ? json_decode($webhookDetails)->hookDetails : $webhookDetails->hookDetails;
41 - $method = isset($details->method) ? $details->method : 'get';
42 - $data = isset($details->url) ? WebHooksHandler::urlParserWrapper($details->url) : false;
43 - $response = null;
44 - if ($data) {
45 - $url = $data['url'];
46 - $params = $data['params'];
47 - $params['entry_id'] = 'test';
48 - switch (strtoupper($method)) {
49 - case 'GET':
50 - $response = HttpHelper::get($url, $params);
51 - break;
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);
51 + }
52 52
53 - case 'POST':
54 - $response = HttpHelper::post($url, $params);
55 - break;
53 + GlobalHelper::requirePostMethod();
56 54
57 - default:
58 - $response = HttpHelper::request($url, $method, $params);
59 - break;
60 - }
61 - }
62 - if (is_wp_error($response)) {
63 - wp_send_json_error(
64 - empty($response) ? 'Unknown Error Occured' : $response->get_error_message(),
65 - 400
66 - );
67 - }
68 - if (empty($data['url'])) {
69 - wp_send_json_error(__('webhook url is empty', 'bit-form'), 400);
70 - }
71 - wp_send_json_success(['msg' => 'webhook executed succcessfully', 'response' => $response], 200);
72 - } else {
73 - wp_send_json_error(
74 - __(
75 - 'Token expired',
76 - 'bit-form'
77 - ),
78 - 401
79 - );
55 + try {
56 + $webhookDetails = GlobalHelper::formatRequestData();
57 + } catch (\InvalidArgumentException $e) {
58 + wp_send_json_error($e->getMessage(), 400);
80 59 }
60 +
61 + $details = is_string($webhookDetails) ? (Utilities::jsonObj($webhookDetails)->hookDetails ?? null) : ($webhookDetails->hookDetails ?? null);
62 +
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 + }
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);
81 78 }
82 79
83 - public function execute(IntegrationHandler $integrationHandler, $integrationDetails, $fieldValues, $entryID, $logID) {
80 + public function execute(IntegrationHandler $integrationHandler, $integrationDetails, $fieldValues, $entryID, $logID)
81 + {
84 82 $details = is_string($integrationDetails->integration_details) ? json_decode($integrationDetails->integration_details) : $integrationDetails->integration_details;
85 - $method = isset($details->method) ? $details->method : 'get';
86 - $data = isset($details->url) ? $this->urlParserWrapper($details->url) : false;
87 - if ($data) {
88 - $url = $data['url'];
89 - $params = $data['params'];
90 - $params = IntegrationHandler::replaceFieldWithValue($params, self::iterate($fieldValues));
91 - $params['entry_id'] = $entryID;
92 - switch (strtoupper($method)) {
93 - case 'GET':
94 - $response = HttpHelper::get($url, $params);
95 - break;
96 83
97 - case 'POST':
98 - $response = HttpHelper::post($url, $params);
99 - break;
84 + $entryDetails = [
85 + 'formId' => $this->formID,
86 + 'entryId' => $entryID,
87 + 'fieldValues' => $fieldValues
88 + ];
100 89
101 - default:
102 - $response = HttpHelper::request($url, $method, $params);
103 - break;
104 - }
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 + }
105 95
106 - // if bitform pro is not active then return the response without response log
107 - if (!class_exists('BitCode\\BitFormPro\\Plugin')) {
108 - return $response;
109 - }
96 + $params = IntegrationHandler::replaceFieldWithValue($data['params'], $fieldValues);
97 + $params['entry_id'] = $entryID;
110 98
111 - // if bitform pro is active then return the response with response log
112 - if (is_wp_error($response)) {
113 - $this->_logResponse->apiResponse($logID, $this->webhookID, ['type' => 'record', 'type_name' => 'web hooks'], 'errors', $response);
114 - } else {
115 - $this->_logResponse->apiResponse($logID, $this->webhookID, ['type' => 'record', 'type_name' => 'web hooks'], 'success', $response);
116 - }
117 - return $response;
118 - } else {
119 - if (!class_exists('BitCode\\BitFormPro\\Plugin')) {
120 - return false;
121 - }
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);
122 101
123 - $this->_logResponse->apiResponse($logID, $this->webhookID, ['type' => 'record', 'type_name' => 'web hooks'], 'errors', 'There is something wrong in the webhook url');
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 124 }
125 125 }
126 126
127 - private static function urlParserWrapper($url) {
128 - if (empty($url)) {
129 - return false;
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'));
130 149 }
150 +
131 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 + }
132 155
133 156 $Scheme = isset($parsedURL['scheme']) ? $parsedURL['scheme'] . '://' : null;
134 - $Usr = isset($parsedURL['usr']) ? $parsedURL['usr'] : null;
157 + $Usr = isset($parsedURL['user']) ? $parsedURL['user'] : null;
135 158 $Pass = isset($parsedURL['pass']) ? ':' . $parsedURL['pass'] : null;
136 - $Host = isset($parsedURL['host']) ? $parsedURL['host'] : null;
159 + $Host = $parsedURL['host'];
137 160 $Port = isset($parsedURL['port']) ? ':' . $parsedURL['port'] : null;
138 161 $Path = isset($parsedURL['path']) ? $parsedURL['path'] : null;
139 - $Query = isset($parsedURL['query']) ? $parsedURL['query'] : null;
162 + $Query = isset($parsedURL['query']) ? $parsedURL['query'] : '';
140 163 $Pass = ($Pass || $Usr) ? "$Pass@" : null;
141 164
142 165 $cleanURL = "$Scheme$Usr$Pass$Host$Port$Path";
143 166 $params = [];
@@ -144,9 +167,13 @@
144 167 foreach (explode('&', $Query) as $keyValue) {
145 168 if (empty($keyValue)) {
146 169 continue;
147 170 }
148 - list($field, $value) = explode('=', $keyValue);
171 + $pair = explode('=', $keyValue, 2);
172 + if (2 !== \count($pair)) {
173 + continue;
174 + }
175 + list($field, $value) = $pair;
149 176 if ('' === trim($value)) {
150 177 continue;
151 178 }
152 179 if (isset($params[$field])) {
@@ -159,12 +186,79 @@
159 186 $params[$field] = sanitize_text_field(urldecode($value));
160 187 }
161 188 }
162 189
190 + if (!wp_http_validate_url($cleanURL)) {
191 + return new \WP_Error('bitform_webhook_url_rejected', self::urlRejectionReason($cleanURL, $parsedURL));
192 + }
193 +
163 194 return ['url' => $cleanURL, 'params' => $params];
164 195 }
165 196
166 - private function iterate($array) {
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;
257 + }
258 +
259 + private function iterate($array)
260 + {
167 261 $ar = [];
168 262 if (is_array($array)) {
169 263 foreach ($array as $k => $v) {
170 264 $ar[$k] = str_replace("\'", "'", $v);