initial
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
<?php
|
||||
/**
|
||||
* Webhook form action.
|
||||
*
|
||||
* POSTs form submission data as JSON to a configured URL.
|
||||
* Uses wp_safe_remote_post() to block requests to internal/private IPs.
|
||||
*
|
||||
* @package GenerateBlocksPro
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Webhook form action class.
|
||||
*/
|
||||
class GenerateBlocks_Pro_Form_Action_Webhook {
|
||||
|
||||
/**
|
||||
* Register the webhook action with the registry.
|
||||
*/
|
||||
public static function init() {
|
||||
GenerateBlocks_Pro_Form_Action_Registry::register( 'webhook', [ __CLASS__, 'send' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the webhook action settings before delivery starts.
|
||||
*
|
||||
* @param array $form_settings The form block settings.
|
||||
* @return true|WP_Error
|
||||
*/
|
||||
public static function validate_settings( $form_settings ) {
|
||||
$url = self::get_valid_webhook_url( $form_settings );
|
||||
|
||||
return is_wp_error( $url ) ? $url : true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the form submission to the configured webhook URL.
|
||||
*
|
||||
* @param array $sanitized_data The sanitized form data (field_name => ['value' => ..., 'label' => ..., 'type' => ...]).
|
||||
* @param array $form_settings The form block settings.
|
||||
* @param array $context Request context (post_id, page_url).
|
||||
* @return true|WP_Error True on success, WP_Error on failure.
|
||||
*/
|
||||
public static function send( $sanitized_data, $form_settings, $context = [] ) {
|
||||
$webhook_url = self::get_valid_webhook_url( $form_settings );
|
||||
|
||||
if ( is_wp_error( $webhook_url ) ) {
|
||||
return $webhook_url;
|
||||
}
|
||||
|
||||
return self::send_to_url( $webhook_url, $sanitized_data, $form_settings, $context );
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate email-signup webhook provider settings.
|
||||
*
|
||||
* @param array $settings Email signup settings.
|
||||
* @return true|WP_Error
|
||||
*/
|
||||
public static function validate_signup_settings( $settings ) {
|
||||
$url = self::get_valid_url( $settings['url'] ?? '' );
|
||||
|
||||
return is_wp_error( $url ) ? $url : true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an email signup submission to a webhook provider URL.
|
||||
*
|
||||
* @param array $sanitized_data The sanitized form data.
|
||||
* @param array $settings Email signup settings.
|
||||
* @param array $context Request context.
|
||||
* @return true|WP_Error
|
||||
*/
|
||||
public static function subscribe( $sanitized_data, $settings, $context = [] ) {
|
||||
$webhook_url = self::get_valid_url( $settings['url'] ?? '' );
|
||||
|
||||
if ( is_wp_error( $webhook_url ) ) {
|
||||
return $webhook_url;
|
||||
}
|
||||
|
||||
$form_settings = [
|
||||
'actionSettings' => [
|
||||
'email-signup' => $settings,
|
||||
'webhook' => [
|
||||
'url' => $settings['url'] ?? '',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
return self::send_to_url( $webhook_url, $sanitized_data, $form_settings, $context );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send sanitized form data to a validated webhook URL.
|
||||
*
|
||||
* @param string $webhook_url Validated webhook URL.
|
||||
* @param array $sanitized_data The sanitized form data.
|
||||
* @param array $form_settings Form settings passed to filters.
|
||||
* @param array $context Request context.
|
||||
* @return true|WP_Error
|
||||
*/
|
||||
private static function send_to_url( $webhook_url, $sanitized_data, $form_settings, $context = [] ) {
|
||||
// Build a clean payload — field name => value (flat), plus metadata.
|
||||
$fields = [];
|
||||
|
||||
foreach ( $sanitized_data as $field_name => $field ) {
|
||||
if ( ! is_array( $field ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$fields[ $field_name ] = (string) ( $field['value'] ?? '' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the webhook payload before sending.
|
||||
*
|
||||
* Field values are sent flat at the top level — receivers like
|
||||
* FluentCRM only map top-level keys (e.g. `email` is required there) —
|
||||
* and repeated under `fields` for consumers that map structured paths.
|
||||
* On a key collision a field value wins the top-level slot.
|
||||
*
|
||||
* @since 2.6.0
|
||||
* @param array $payload The webhook payload.
|
||||
* @param array $sanitized_data The full sanitized form data with labels and types.
|
||||
* @param array $form_settings The form block settings.
|
||||
* @param array $context Request context.
|
||||
*/
|
||||
$payload = apply_filters(
|
||||
'generateblocks_form_webhook_payload',
|
||||
array_merge(
|
||||
[
|
||||
'form_name' => ! empty( $context['form_id'] ) ? get_the_title( $context['form_id'] ) : '',
|
||||
'fields' => $fields,
|
||||
'page_url' => $context['page_url'] ?? '',
|
||||
'timestamp' => gmdate( 'c' ),
|
||||
],
|
||||
$fields
|
||||
),
|
||||
$sanitized_data,
|
||||
$form_settings,
|
||||
$context
|
||||
);
|
||||
|
||||
// wp_safe_remote_post blocks requests to internal/private IPs (127.0.0.1,
|
||||
// 10.x.x.x, 169.254.169.254, etc.) via wp_http_validate_url(). This prevents
|
||||
// SSRF since the webhook URL is user-supplied. Redirection is disabled so a
|
||||
// 302 to an internal IP can't bypass the validation.
|
||||
$response = wp_safe_remote_post(
|
||||
$webhook_url,
|
||||
[
|
||||
'body' => wp_json_encode( $payload ),
|
||||
'headers' => [ 'Content-Type' => 'application/json' ],
|
||||
'timeout' => GenerateBlocks_Pro_Form_Http::DEFAULT_TIMEOUT,
|
||||
'redirection' => 0,
|
||||
'data_format' => 'body',
|
||||
]
|
||||
);
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return new WP_Error(
|
||||
'webhook_failed',
|
||||
'Webhook request failed: ' . $response->get_error_message()
|
||||
);
|
||||
}
|
||||
|
||||
$status_code = wp_remote_retrieve_response_code( $response );
|
||||
|
||||
// Accept any 2xx response.
|
||||
if ( $status_code < 200 || $status_code >= 300 ) {
|
||||
return new WP_Error(
|
||||
'webhook_failed',
|
||||
'Webhook returned status ' . $status_code . '.'
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validated webhook URL.
|
||||
*
|
||||
* @param array $form_settings The form block settings.
|
||||
* @return string|WP_Error
|
||||
*/
|
||||
private static function get_valid_webhook_url( $form_settings ) {
|
||||
$webhook_url = $form_settings['actionSettings']['webhook']['url'] ?? '';
|
||||
|
||||
return self::get_valid_url( $webhook_url );
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a webhook URL.
|
||||
*
|
||||
* @param mixed $webhook_url Raw webhook URL.
|
||||
* @return string|WP_Error
|
||||
*/
|
||||
private static function get_valid_url( $webhook_url ) {
|
||||
if ( empty( $webhook_url ) ) {
|
||||
return new WP_Error(
|
||||
'missing_webhook_url',
|
||||
'Webhook URL is not configured.',
|
||||
[ 'status' => 400 ]
|
||||
);
|
||||
}
|
||||
|
||||
$webhook_url = esc_url_raw( $webhook_url, [ 'https', 'http' ] );
|
||||
|
||||
if ( empty( $webhook_url ) || ! wp_http_validate_url( $webhook_url ) ) {
|
||||
return new WP_Error(
|
||||
'invalid_webhook_url',
|
||||
'Invalid webhook URL.',
|
||||
[ 'status' => 400 ]
|
||||
);
|
||||
}
|
||||
|
||||
return $webhook_url;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user