PluginProbe ʕ •ᴥ•ʔ
Superb Addons: Blocks, Patterns, Pre-built Pages, Sliders, Popups, Free Forms, Animations & More / 4.0.0
Superb Addons: Blocks, Patterns, Pre-built Pages, Sliders, Popups, Free Forms, Animations & More v4.0.0
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 2 months ago class-form-captcha-handler.php 2 months ago class-form-controller.php 2 months ago class-form-email-config-check.php 2 months ago class-form-email-handler.php 2 months ago class-form-encryption.php 2 months ago class-form-exporter.php 2 months ago class-form-field-validator.php 2 months ago class-form-file-handler.php 2 months ago class-form-google-auth.php 2 months ago class-form-integration-handler.php 2 months ago class-form-math-parser.php 2 months ago class-form-permissions.php 2 months ago class-form-registry.php 2 months ago class-form-settings.php 2 months ago class-form-submission-cpt.php 2 months ago class-form-submission-handler.php 2 months ago
class-form-integration-handler.php
633 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 $header_labels[] = $label;
389 $row_values[] = (string) $value;
390 }
391
392 $base_url = 'https://sheets.googleapis.com/v4/spreadsheets/' . urlencode($spreadsheet_id);
393 $auth_headers = array(
394 'Authorization' => 'Bearer ' . $token,
395 'Content-Type' => 'application/json',
396 );
397
398 // Check if header row exists
399 $check_url = $base_url . '/values/' . urlencode($range . '!1:1');
400 $check_response = wp_remote_get($check_url, array(
401 'headers' => $auth_headers,
402 'timeout' => 15,
403 ));
404
405 if (is_wp_error($check_response)) {
406 return array('sent' => false, 'error' => $check_response->get_error_message());
407 }
408
409 $check_code = wp_remote_retrieve_response_code($check_response);
410 if ($check_code < 200 || $check_code >= 300) {
411 return array('sent' => false, 'error' => sprintf('Google Sheets API returned HTTP %d', $check_code));
412 }
413
414 $check_body = json_decode(wp_remote_retrieve_body($check_response), true);
415 $existing_headers = isset($check_body['values'][0]) ? $check_body['values'][0] : array();
416
417 $values_to_append = array();
418
419 if (empty($existing_headers)) {
420 // No headers yet: prepend header row (note: concurrent first submissions may duplicate this row)
421 $values_to_append[] = $header_labels;
422 $values_to_append[] = $row_values;
423 } else {
424 // Match columns by existing header labels
425 $ordered_values = array();
426 foreach ($existing_headers as $existing_label) {
427 $idx = array_search($existing_label, $header_labels, true);
428 if ($idx !== false) {
429 $ordered_values[] = $row_values[$idx];
430 } else {
431 $ordered_values[] = '';
432 }
433 }
434
435 // Detect new labels not yet in the header row and append them
436 $new_labels = array();
437 $new_values = array();
438 foreach ($header_labels as $i => $label) {
439 if (!in_array($label, $existing_headers, true)) {
440 $new_labels[] = $label;
441 $new_values[] = $row_values[$i];
442 }
443 }
444
445 if (!empty($new_labels)) {
446 // Update the header row to include new columns
447 $updated_headers = array_merge($existing_headers, $new_labels);
448 $update_url = $base_url . '/values/' . urlencode($range . '!1:1') . '?valueInputOption=RAW';
449 wp_remote_request($update_url, array(
450 'method' => 'PUT',
451 'headers' => $auth_headers,
452 'body' => wp_json_encode(array('values' => array($updated_headers))),
453 'timeout' => 15,
454 ));
455 $ordered_values = array_merge($ordered_values, $new_values);
456 }
457
458 $values_to_append[] = $ordered_values;
459 }
460
461 // Append values
462 $append_url = $base_url . '/values/' . urlencode($range) . ':append?valueInputOption=USER_ENTERED';
463 $append_response = wp_remote_post($append_url, array(
464 'headers' => $auth_headers,
465 'body' => wp_json_encode(array('values' => $values_to_append)),
466 'timeout' => 15,
467 ));
468
469 if (is_wp_error($append_response)) {
470 return array('sent' => false, 'error' => $append_response->get_error_message());
471 }
472
473 $append_code = wp_remote_retrieve_response_code($append_response);
474 $success = $append_code >= 200 && $append_code < 300;
475 return array(
476 'sent' => $success,
477 'error' => $success ? null : sprintf('Google Sheets API returned HTTP %d', $append_code),
478 );
479 }
480
481 /**
482 * Send form submission notification to Slack via Incoming Webhook.
483 *
484 * @param string $webhook_url Slack Incoming Webhook URL.
485 * @param string $form_name Form name.
486 * @param array $fields Submitted fields (field_id => value).
487 * @param array $form_fields_config Form field definitions.
488 * @return array ['sent' => bool, 'error' => string|null]
489 */
490 public static function SendToSlack($webhook_url, $form_name, $fields, $form_fields_config)
491 {
492 if (empty($webhook_url)) {
493 return array('sent' => false, 'error' => 'No webhook URL provided.');
494 }
495
496 $validated_url = self::ValidateOutboundUrl($webhook_url);
497 if (!$validated_url) {
498 return array('sent' => false, 'error' => __('Invalid or blocked URL.', 'superb-blocks'));
499 }
500
501 $sensitive_field_ids = self::GetSensitiveFieldIds($form_fields_config);
502
503 // Build Slack Block Kit message
504 $blocks = array();
505 $blocks[] = array(
506 'type' => 'header',
507 'text' => array(
508 'type' => 'plain_text',
509 'text' => sprintf(
510 /* translators: %s: form name */
511 __('New submission: %s', 'superb-blocks'),
512 $form_name ? $form_name : __('Unnamed Form', 'superb-blocks')
513 ),
514 'emoji' => true,
515 ),
516 );
517
518 foreach ($form_fields_config as $field_def) {
519 $fid = isset($field_def['fieldId']) ? $field_def['fieldId'] : '';
520 if (empty($fid) || in_array($fid, $sensitive_field_ids, true)) {
521 continue;
522 }
523
524 $label = isset($field_def['label']) ? $field_def['label'] : $fid;
525 $type = isset($field_def['fieldType']) ? $field_def['fieldType'] : 'text';
526 $value = isset($fields[$fid]) ? $fields[$fid] : '';
527
528 if ($type === 'file' && is_array($value)) {
529 $value = implode(', ', self::ExtractFilenames($value));
530 }
531
532 if (is_array($value)) {
533 $value = wp_json_encode($value);
534 }
535
536 $escaped_label = self::EscapeSlackMrkdwn($label);
537 $escaped_value = $value !== '' ? self::EscapeSlackMrkdwn($value) : '_empty_';
538
539 $blocks[] = array(
540 'type' => 'section',
541 'text' => array(
542 'type' => 'mrkdwn',
543 'text' => '*' . $escaped_label . '*' . "\n" . $escaped_value,
544 ),
545 );
546 }
547
548 $payload = array('blocks' => $blocks);
549
550 $response = wp_remote_post($validated_url, array(
551 'headers' => array('Content-Type' => 'application/json'),
552 'body' => wp_json_encode($payload),
553 'timeout' => 15,
554 ));
555
556 if (is_wp_error($response)) {
557 return array('sent' => false, 'error' => $response->get_error_message());
558 }
559
560 $code = wp_remote_retrieve_response_code($response);
561 $success = $code === 200;
562 return array(
563 'sent' => $success,
564 'error' => $success ? null : sprintf('Slack returned HTTP %d', $code),
565 );
566 }
567
568 /**
569 * Send contact to Brevo (Sendinblue).
570 *
571 * @param array $list_ids List IDs to subscribe to.
572 * @param string $email
573 * @param array $fields Additional attributes
574 * @return bool
575 */
576 public static function SendToBrevo($list_ids, $email, $fields = array())
577 {
578 $api_key = FormSettings::Get(FormSettings::OPTION_BREVO_API_KEY);
579 if (empty($api_key) || empty($email)) {
580 return false;
581 }
582
583 if (!is_array($list_ids)) {
584 $list_ids = array($list_ids);
585 }
586
587 $int_list_ids = array();
588 foreach ($list_ids as $lid) {
589 if (!empty($lid)) {
590 $int_list_ids[] = intval($lid);
591 }
592 }
593 if (empty($int_list_ids)) {
594 return false;
595 }
596
597 $url = 'https://api.brevo.com/v3/contacts';
598
599 $body = array(
600 'email' => sanitize_email($email),
601 'listIds' => $int_list_ids,
602 'updateEnabled' => true,
603 );
604
605 if (!empty($fields)) {
606 $attributes = array();
607 foreach ($fields as $key => $value) {
608 $attributes[strtoupper(sanitize_key($key))] = sanitize_text_field($value);
609 }
610 if (!empty($attributes)) {
611 $body['attributes'] = $attributes;
612 }
613 }
614
615 $response = wp_remote_post($url, array(
616 'headers' => array(
617 'api-key' => $api_key,
618 'Content-Type' => 'application/json',
619 'Accept' => 'application/json',
620 ),
621 'body' => wp_json_encode($body),
622 'timeout' => 15,
623 ));
624
625 if (is_wp_error($response)) {
626 return false;
627 }
628
629 $code = wp_remote_retrieve_response_code($response);
630 return $code === 201 || $code === 204;
631 }
632 }
633