PluginProbe ʕ •ᴥ•ʔ
Superb Addons: Blocks, Patterns, Pre-built Pages, Sliders, Popups, Free Forms, Animations & More / 4.0.7
Superb Addons: Blocks, Patterns, Pre-built Pages, Sliders, Popups, Free Forms, Animations & More v4.0.7
4.0.7 4.0.6 4.0.5 4.0.4 4.0.3 4.0.2 4.0.1 4.0.0 trunk 1.0.0 2.0.0 2.0.1 2.0.2 2.0.3 3.0 3.0.1 3.0.2 3.0.5 3.0.6 3.0.7 3.0.8 3.0.9 3.1.0 3.1.2 3.1.3 3.2.0 3.2.1 3.2.2 3.2.4 3.2.5 3.2.7 3.2.8 3.2.9 3.3.0 3.3.1 3.3.2 3.4.0 3.4.1 3.4.2 3.4.5 3.4.6 3.5.0 3.5.1 3.5.2 3.5.3 3.5.4 3.5.6 3.5.7 3.5.8 3.5.9 3.6.0 3.6.1 3.6.2 3.7.0 3.7.1
superb-blocks / src / gutenberg / form / class-form-integration-handler.php
superb-blocks / src / gutenberg / form Last commit date
class-form-access-control.php 5 days ago class-form-captcha-handler.php 5 days ago class-form-controller.php 5 days ago class-form-email-config-check.php 5 days ago class-form-email-handler.php 5 days ago class-form-encryption.php 5 days ago class-form-exporter.php 5 days ago class-form-field-validator.php 5 days ago class-form-file-handler.php 5 days ago class-form-google-auth.php 5 days ago class-form-integration-handler.php 5 days ago class-form-math-parser.php 5 days ago class-form-permissions.php 5 days ago class-form-registry.php 5 days ago class-form-settings.php 5 days ago class-form-submission-cpt.php 5 days ago class-form-submission-handler.php 5 days ago
class-form-integration-handler.php
659 lines
1 <?php
2
3 namespace SuperbAddons\Gutenberg\Form;
4
5 defined('ABSPATH') || exit();
6
7 class FormIntegrationHandler
8 {
9 /**
10 * Get field IDs that should be excluded from external integrations.
11 *
12 * @param array $form_fields_config Form field definitions.
13 * @return array Field IDs to exclude.
14 */
15 private static function GetSensitiveFieldIds($form_fields_config)
16 {
17 $ids = array();
18 foreach ($form_fields_config as $field_def) {
19 if (!empty($field_def['sensitive']) || (!empty($field_def['fieldType']) && $field_def['fieldType'] === 'signature')) {
20 $ids[] = isset($field_def['fieldId']) ? $field_def['fieldId'] : '';
21 }
22 }
23 return $ids;
24 }
25
26 /**
27 * Extract filenames from a file field value.
28 *
29 * @param array $file_value Array of file data arrays.
30 * @return array List of filenames.
31 */
32 private static function ExtractFilenames($file_value)
33 {
34 $filenames = array();
35 foreach ($file_value as $file) {
36 $filenames[] = isset($file['name']) ? $file['name'] : '';
37 }
38 return $filenames;
39 }
40
41 /**
42 * Escape text for safe use in Slack mrkdwn.
43 *
44 * @param string $text Raw text.
45 * @return string Escaped text.
46 */
47 private static function EscapeSlackMrkdwn($text)
48 {
49 return str_replace(
50 array('&', '<', '>'),
51 array('&amp;', '&lt;', '&gt;'),
52 $text
53 );
54 }
55
56 /**
57 * Validate a URL for safe outbound HTTP requests (blocks private/internal IPs).
58 *
59 * @param string $url URL to validate.
60 * @return string|false Validated URL or false if blocked.
61 */
62 private static function ValidateOutboundUrl($url)
63 {
64 $url = esc_url_raw($url);
65 if (empty($url)) {
66 return false;
67 }
68 return wp_http_validate_url($url);
69 }
70
71 /**
72 * Fetch available Mailchimp lists/audiences.
73 *
74 * @return array|\WP_Error Array of {id, name} on success, WP_Error on failure.
75 */
76 public static function GetMailchimpLists()
77 {
78 $api_key = FormSettings::Get(FormSettings::OPTION_MAILCHIMP_API_KEY);
79 if (empty($api_key)) {
80 return new \WP_Error('no_api_key', __('Mailchimp API key is not configured.', 'superb-blocks'), array('status' => 400));
81 }
82
83 $dc_parts = explode('-', $api_key);
84 if (count($dc_parts) < 2) {
85 return new \WP_Error('api_error', __('Invalid Mailchimp API key format.', 'superb-blocks'), array('status' => 400));
86 }
87 $dc = $dc_parts[1];
88
89 $url = 'https://' . $dc . '.api.mailchimp.com/3.0/lists?count=100&fields=lists.id,lists.name';
90
91 $response = wp_remote_get($url, array(
92 'headers' => array(
93 'Authorization' => 'Basic ' . base64_encode('anystring:' . $api_key),
94 ),
95 'timeout' => 15,
96 ));
97
98 if (is_wp_error($response)) {
99 return new \WP_Error('api_error', __('Failed to connect to Mailchimp.', 'superb-blocks'), array('status' => 502));
100 }
101
102 $code = wp_remote_retrieve_response_code($response);
103 if ($code !== 200) {
104 return new \WP_Error('api_error', __('Mailchimp API returned an error.', 'superb-blocks'), array('status' => $code));
105 }
106
107 $body = json_decode(wp_remote_retrieve_body($response), true);
108 $lists = array();
109 if (isset($body['lists']) && is_array($body['lists'])) {
110 foreach ($body['lists'] as $list) {
111 $lists[] = array(
112 'id' => isset($list['id']) ? sanitize_text_field($list['id']) : '',
113 'name' => isset($list['name']) ? sanitize_text_field($list['name']) : '',
114 );
115 }
116 }
117
118 return $lists;
119 }
120
121 /**
122 * Fetch available Brevo (Sendinblue) lists.
123 *
124 * @return array|\WP_Error Array of {id, name} on success, WP_Error on failure.
125 */
126 public static function GetBrevoLists()
127 {
128 $api_key = FormSettings::Get(FormSettings::OPTION_BREVO_API_KEY);
129 if (empty($api_key)) {
130 return new \WP_Error('no_api_key', __('Brevo API key is not configured.', 'superb-blocks'), array('status' => 400));
131 }
132
133 $url = 'https://api.brevo.com/v3/contacts/lists?limit=50&offset=0';
134
135 $response = wp_remote_get($url, array(
136 'headers' => array(
137 'api-key' => $api_key,
138 'Accept' => 'application/json',
139 ),
140 'timeout' => 15,
141 ));
142
143 if (is_wp_error($response)) {
144 return new \WP_Error('api_error', __('Failed to connect to Brevo.', 'superb-blocks'), array('status' => 502));
145 }
146
147 $code = wp_remote_retrieve_response_code($response);
148 if ($code !== 200) {
149 return new \WP_Error('api_error', __('Brevo API returned an error.', 'superb-blocks'), array('status' => $code));
150 }
151
152 $body = json_decode(wp_remote_retrieve_body($response), true);
153 $lists = array();
154 if (isset($body['lists']) && is_array($body['lists'])) {
155 foreach ($body['lists'] as $list) {
156 $lists[] = array(
157 'id' => isset($list['id']) ? intval($list['id']) : 0,
158 'name' => isset($list['name']) ? sanitize_text_field($list['name']) : '',
159 );
160 }
161 }
162
163 return $lists;
164 }
165
166 /**
167 * Send subscriber to Mailchimp.
168 *
169 * @param array $list_ids List/audience IDs to subscribe to.
170 * @param string $email
171 * @param array $fields Additional merge fields
172 * @return bool
173 */
174 public static function SendToMailchimp($list_ids, $email, $fields = array())
175 {
176 $api_key = FormSettings::Get(FormSettings::OPTION_MAILCHIMP_API_KEY);
177 if (empty($api_key) || empty($email)) {
178 return false;
179 }
180
181 if (!is_array($list_ids)) {
182 $list_ids = array($list_ids);
183 }
184
185 $dc_parts = explode('-', $api_key);
186 if (count($dc_parts) < 2) {
187 return false;
188 }
189 $dc = $dc_parts[1];
190
191 $merge_fields = array();
192 if (!empty($fields)) {
193 foreach ($fields as $key => $value) {
194 $merge_fields[strtoupper(sanitize_key($key))] = sanitize_text_field($value);
195 }
196 }
197
198 $success = false;
199 foreach ($list_ids as $list_id) {
200 if (empty($list_id)) {
201 continue;
202 }
203
204 $url = 'https://' . $dc . '.api.mailchimp.com/3.0/lists/' . urlencode($list_id) . '/members';
205
206 $body = array(
207 'email_address' => sanitize_email($email),
208 'status' => 'subscribed',
209 );
210
211 if (!empty($merge_fields)) {
212 $body['merge_fields'] = $merge_fields;
213 }
214
215 $response = wp_remote_post($url, array(
216 'headers' => array(
217 'Authorization' => 'Basic ' . base64_encode('anystring:' . $api_key),
218 'Content-Type' => 'application/json',
219 ),
220 'body' => wp_json_encode($body),
221 'timeout' => 15,
222 ));
223
224 if (!is_wp_error($response)) {
225 $code = wp_remote_retrieve_response_code($response);
226 if ($code === 200 || $code === 204) {
227 $success = true;
228 }
229 }
230 }
231
232 return $success;
233 }
234
235 /**
236 * Send form submission data to a webhook URL.
237 *
238 * @param string $url Webhook URL.
239 * @param string $method HTTP method (POST, PUT, PATCH).
240 * @param string $form_id Form ID.
241 * @param string $form_name Form name.
242 * @param array $fields Submitted fields (field_id => value).
243 * @param array $form_fields_config Form field definitions.
244 * @param string $secret HMAC signing secret.
245 * @param array $headers Custom headers [{key, value}].
246 * @return array ['sent' => bool, 'code' => int, 'error' => string|null]
247 */
248 public static function SendWebhook($url, $method, $form_id, $form_name, $fields, $form_fields_config, $secret = '', $headers = array())
249 {
250 if (empty($url)) {
251 return array('sent' => false, 'code' => 0, 'error' => 'No URL provided.');
252 }
253
254 $validated_url = self::ValidateOutboundUrl($url);
255 if (!$validated_url) {
256 return array('sent' => false, 'code' => 0, 'error' => __('Invalid or blocked URL.', 'superb-blocks'));
257 }
258
259 $allowed_methods = array('POST', 'PUT', 'PATCH');
260 if (!in_array(strtoupper($method), $allowed_methods, true)) {
261 $method = 'POST';
262 }
263
264 // Build field data for payload
265 $payload_fields = array();
266 $sensitive_field_ids = self::GetSensitiveFieldIds($form_fields_config);
267
268 foreach ($form_fields_config as $field_def) {
269 $fid = isset($field_def['fieldId']) ? $field_def['fieldId'] : '';
270 if (empty($fid) || in_array($fid, $sensitive_field_ids, true)) {
271 continue;
272 }
273
274 $label = isset($field_def['label']) ? $field_def['label'] : $fid;
275 $type = isset($field_def['fieldType']) ? $field_def['fieldType'] : 'text';
276 $value = isset($fields[$fid]) ? $fields[$fid] : '';
277
278 if ($type === 'file' && is_array($value)) {
279 $value = self::ExtractFilenames($value);
280 }
281
282 $payload_fields[$fid] = array(
283 'label' => $label,
284 'value' => $value,
285 'type' => $type,
286 );
287 }
288
289 $payload = array(
290 'form_id' => $form_id,
291 'form_name' => $form_name,
292 'submitted_at' => gmdate('c'),
293 'fields' => $payload_fields,
294 );
295
296 $json = wp_json_encode($payload);
297
298 $request_headers = array(
299 'Content-Type' => 'application/json',
300 'User-Agent' => 'SuperbAddons/' . SUPERBADDONS_VERSION,
301 );
302
303 // HMAC signature
304 if (!empty($secret)) {
305 $request_headers['X-Superb-Signature'] = 'sha256=' . hash_hmac('sha256', $json, $secret);
306 }
307
308 // Custom headers
309 if (is_array($headers)) {
310 foreach ($headers as $h) {
311 if (!empty($h['key'])) {
312 $request_headers[sanitize_text_field($h['key'])] = sanitize_text_field(isset($h['value']) ? $h['value'] : '');
313 }
314 }
315 }
316
317 $response = wp_remote_request($validated_url, array(
318 'method' => strtoupper($method),
319 'headers' => $request_headers,
320 'body' => $json,
321 'timeout' => 15,
322 ));
323
324 if (is_wp_error($response)) {
325 return array('sent' => false, 'code' => 0, 'error' => $response->get_error_message());
326 }
327
328 $code = wp_remote_retrieve_response_code($response);
329 $success = $code >= 200 && $code < 300;
330 return array(
331 'sent' => $success,
332 'code' => $code,
333 'error' => $success ? null : sprintf('HTTP %d', $code),
334 );
335 }
336
337 /**
338 * Send form submission data to Google Sheets.
339 *
340 * @param string $spreadsheet_id_or_url Spreadsheet ID or full URL.
341 * @param string $sheet_name Sheet name (empty for first sheet).
342 * @param array $fields Submitted fields (field_id => value).
343 * @param array $form_fields_config Form field definitions.
344 * @return array ['sent' => bool, 'error' => string|null]
345 */
346 public static function SendToGoogleSheets($spreadsheet_id_or_url, $sheet_name, $fields, $form_fields_config)
347 {
348 // Extract spreadsheet ID from URL if needed
349 $spreadsheet_id = $spreadsheet_id_or_url;
350 if (preg_match('/\/spreadsheets\/d\/([a-zA-Z0-9_-]+)/', $spreadsheet_id_or_url, $matches)) {
351 $spreadsheet_id = $matches[1];
352 }
353
354 if (empty($spreadsheet_id)) {
355 return array('sent' => false, 'error' => 'No spreadsheet ID provided.');
356 }
357
358 $token = FormGoogleAuth::GetAccessToken();
359 if (is_wp_error($token)) {
360 return array('sent' => false, 'error' => $token->get_error_message());
361 }
362
363 $range = !empty($sheet_name) ? $sheet_name : 'Sheet1';
364
365 // Build ordered labels and values from form field config
366 $sensitive_field_ids = self::GetSensitiveFieldIds($form_fields_config);
367
368 $header_labels = array();
369 $row_values = array();
370 foreach ($form_fields_config as $field_def) {
371 $fid = isset($field_def['fieldId']) ? $field_def['fieldId'] : '';
372 if (empty($fid) || in_array($fid, $sensitive_field_ids, true)) {
373 continue;
374 }
375
376 $label = isset($field_def['label']) ? $field_def['label'] : $fid;
377 $type = isset($field_def['fieldType']) ? $field_def['fieldType'] : 'text';
378 $value = isset($fields[$fid]) ? $fields[$fid] : '';
379
380 if ($type === 'file' && is_array($value)) {
381 $value = implode(', ', self::ExtractFilenames($value));
382 }
383
384 if (is_array($value)) {
385 $value = wp_json_encode($value);
386 }
387
388 // Ensure unique column headers so two fields sharing a label don't collide
389 // when values are matched back to columns by header text below.
390 $base_label = $label;
391 $suffix = 2;
392 while (in_array($label, $header_labels, true)) {
393 $label = $base_label . ' (' . $suffix . ')';
394 $suffix++;
395 }
396
397 $header_labels[] = $label;
398 $row_values[] = (string) $value;
399 }
400
401 $base_url = 'https://sheets.googleapis.com/v4/spreadsheets/' . rawurlencode($spreadsheet_id);
402 $auth_headers = array(
403 'Authorization' => 'Bearer ' . $token,
404 'Content-Type' => 'application/json',
405 );
406
407 // A1 notation requires sheet names with spaces or special characters to be wrapped in
408 // single quotes (embedded quotes doubled). Use rawurlencode for path segments so a space
409 // becomes %20, not '+' (which is a literal plus in a URL path and would target the wrong sheet).
410 $quoted_sheet = "'" . str_replace("'", "''", $range) . "'";
411
412 // Check if header row exists
413 $check_url = $base_url . '/values/' . rawurlencode($quoted_sheet . '!1:1');
414 $check_response = wp_remote_get($check_url, array(
415 'headers' => $auth_headers,
416 'timeout' => 15,
417 ));
418
419 if (is_wp_error($check_response)) {
420 return array('sent' => false, 'error' => $check_response->get_error_message());
421 }
422
423 $check_code = wp_remote_retrieve_response_code($check_response);
424 if ($check_code < 200 || $check_code >= 300) {
425 return array('sent' => false, 'error' => sprintf('Google Sheets API returned HTTP %d', $check_code));
426 }
427
428 $check_body = json_decode(wp_remote_retrieve_body($check_response), true);
429 $existing_headers = isset($check_body['values'][0]) ? $check_body['values'][0] : array();
430
431 $values_to_append = array();
432
433 if (empty($existing_headers)) {
434 // No headers yet: prepend header row (note: concurrent first submissions may duplicate this row)
435 $values_to_append[] = $header_labels;
436 $values_to_append[] = $row_values;
437 } else {
438 // Match columns by existing header labels
439 $ordered_values = array();
440 foreach ($existing_headers as $existing_label) {
441 $idx = array_search($existing_label, $header_labels, true);
442 if ($idx !== false) {
443 $ordered_values[] = $row_values[$idx];
444 } else {
445 $ordered_values[] = '';
446 }
447 }
448
449 // Detect new labels not yet in the header row and append them
450 $new_labels = array();
451 $new_values = array();
452 foreach ($header_labels as $i => $label) {
453 if (!in_array($label, $existing_headers, true)) {
454 $new_labels[] = $label;
455 $new_values[] = $row_values[$i];
456 }
457 }
458
459 if (!empty($new_labels)) {
460 // Update the header row to include new columns
461 $updated_headers = array_merge($existing_headers, $new_labels);
462 $update_url = $base_url . '/values/' . rawurlencode($quoted_sheet . '!1:1') . '?valueInputOption=RAW';
463 $update_response = wp_remote_request($update_url, array(
464 'method' => 'PUT',
465 'headers' => $auth_headers,
466 'body' => wp_json_encode(array('values' => array($updated_headers))),
467 'timeout' => 15,
468 ));
469
470 // If the header could not be extended, bail rather than appending values whose
471 // columns have no header, which would land permanently misaligned in the sheet.
472 if (is_wp_error($update_response)) {
473 return array('sent' => false, 'error' => $update_response->get_error_message());
474 }
475 $update_code = wp_remote_retrieve_response_code($update_response);
476 if ($update_code < 200 || $update_code >= 300) {
477 return array('sent' => false, 'error' => sprintf('Google Sheets API returned HTTP %d', $update_code));
478 }
479
480 $ordered_values = array_merge($ordered_values, $new_values);
481 }
482
483 $values_to_append[] = $ordered_values;
484 }
485
486 // Append values. RAW (not USER_ENTERED) so submitted text is stored literally and a
487 // value beginning with = + - @ is never evaluated as a spreadsheet formula (CSV injection).
488 $append_url = $base_url . '/values/' . rawurlencode($quoted_sheet) . ':append?valueInputOption=RAW';
489 $append_response = wp_remote_post($append_url, array(
490 'headers' => $auth_headers,
491 'body' => wp_json_encode(array('values' => $values_to_append)),
492 'timeout' => 15,
493 ));
494
495 if (is_wp_error($append_response)) {
496 return array('sent' => false, 'error' => $append_response->get_error_message());
497 }
498
499 $append_code = wp_remote_retrieve_response_code($append_response);
500 $success = $append_code >= 200 && $append_code < 300;
501 return array(
502 'sent' => $success,
503 'error' => $success ? null : sprintf('Google Sheets API returned HTTP %d', $append_code),
504 );
505 }
506
507 /**
508 * Send form submission notification to Slack via Incoming Webhook.
509 *
510 * @param string $webhook_url Slack Incoming Webhook URL.
511 * @param string $form_name Form name.
512 * @param array $fields Submitted fields (field_id => value).
513 * @param array $form_fields_config Form field definitions.
514 * @return array ['sent' => bool, 'error' => string|null]
515 */
516 public static function SendToSlack($webhook_url, $form_name, $fields, $form_fields_config)
517 {
518 if (empty($webhook_url)) {
519 return array('sent' => false, 'error' => 'No webhook URL provided.');
520 }
521
522 $validated_url = self::ValidateOutboundUrl($webhook_url);
523 if (!$validated_url) {
524 return array('sent' => false, 'error' => __('Invalid or blocked URL.', 'superb-blocks'));
525 }
526
527 $sensitive_field_ids = self::GetSensitiveFieldIds($form_fields_config);
528
529 // Build Slack Block Kit message
530 $blocks = array();
531 $blocks[] = array(
532 'type' => 'header',
533 'text' => array(
534 'type' => 'plain_text',
535 'text' => sprintf(
536 /* translators: %s: form name */
537 __('New submission: %s', 'superb-blocks'),
538 $form_name ? $form_name : __('Unnamed Form', 'superb-blocks')
539 ),
540 'emoji' => true,
541 ),
542 );
543
544 foreach ($form_fields_config as $field_def) {
545 $fid = isset($field_def['fieldId']) ? $field_def['fieldId'] : '';
546 if (empty($fid) || in_array($fid, $sensitive_field_ids, true)) {
547 continue;
548 }
549
550 $label = isset($field_def['label']) ? $field_def['label'] : $fid;
551 $type = isset($field_def['fieldType']) ? $field_def['fieldType'] : 'text';
552 $value = isset($fields[$fid]) ? $fields[$fid] : '';
553
554 if ($type === 'file' && is_array($value)) {
555 $value = implode(', ', self::ExtractFilenames($value));
556 }
557
558 if (is_array($value)) {
559 $value = wp_json_encode($value);
560 }
561
562 $escaped_label = self::EscapeSlackMrkdwn($label);
563 $escaped_value = $value !== '' ? self::EscapeSlackMrkdwn($value) : '_empty_';
564
565 $blocks[] = array(
566 'type' => 'section',
567 'text' => array(
568 'type' => 'mrkdwn',
569 'text' => '*' . $escaped_label . '*' . "\n" . $escaped_value,
570 ),
571 );
572 }
573
574 $payload = array('blocks' => $blocks);
575
576 $response = wp_remote_post($validated_url, array(
577 'headers' => array('Content-Type' => 'application/json'),
578 'body' => wp_json_encode($payload),
579 'timeout' => 15,
580 ));
581
582 if (is_wp_error($response)) {
583 return array('sent' => false, 'error' => $response->get_error_message());
584 }
585
586 $code = wp_remote_retrieve_response_code($response);
587 $success = $code === 200;
588 return array(
589 'sent' => $success,
590 'error' => $success ? null : sprintf('Slack returned HTTP %d', $code),
591 );
592 }
593
594 /**
595 * Send contact to Brevo (Sendinblue).
596 *
597 * @param array $list_ids List IDs to subscribe to.
598 * @param string $email
599 * @param array $fields Additional attributes
600 * @return bool
601 */
602 public static function SendToBrevo($list_ids, $email, $fields = array())
603 {
604 $api_key = FormSettings::Get(FormSettings::OPTION_BREVO_API_KEY);
605 if (empty($api_key) || empty($email)) {
606 return false;
607 }
608
609 if (!is_array($list_ids)) {
610 $list_ids = array($list_ids);
611 }
612
613 $int_list_ids = array();
614 foreach ($list_ids as $lid) {
615 if (!empty($lid)) {
616 $int_list_ids[] = intval($lid);
617 }
618 }
619 if (empty($int_list_ids)) {
620 return false;
621 }
622
623 $url = 'https://api.brevo.com/v3/contacts';
624
625 $body = array(
626 'email' => sanitize_email($email),
627 'listIds' => $int_list_ids,
628 'updateEnabled' => true,
629 );
630
631 if (!empty($fields)) {
632 $attributes = array();
633 foreach ($fields as $key => $value) {
634 $attributes[strtoupper(sanitize_key($key))] = sanitize_text_field($value);
635 }
636 if (!empty($attributes)) {
637 $body['attributes'] = $attributes;
638 }
639 }
640
641 $response = wp_remote_post($url, array(
642 'headers' => array(
643 'api-key' => $api_key,
644 'Content-Type' => 'application/json',
645 'Accept' => 'application/json',
646 ),
647 'body' => wp_json_encode($body),
648 'timeout' => 15,
649 ));
650
651 if (is_wp_error($response)) {
652 return false;
653 }
654
655 $code = wp_remote_retrieve_response_code($response);
656 return $code === 201 || $code === 204;
657 }
658 }
659