) and its fields are the variables, named like the field's "name" setting, * or field when it has none (a name with characters outside A-Z, a-z, 0-9 and _:.- becomes * field_). File upload, captcha and submit button are not variables. * * Simple Form block (kadence/form): it sits inside any post, page or widget, and finding every form would mean * parsing every post. So all simple forms share ONE event (kadence.simple_form), and a field is the variable * field_, n being its place in the form (the same numbering Kadence uses in its e-mail subjects, {field_1}). * The editor offers field_1 to field_10; the submission fills as many as the form has. * * Both handlers call their hook only for an accepted submission: after the required-field checks, the honeypot * (simple form) and the CAPTCHA, and, for the Advanced Form, the spam filter kadence_blocks_advanced_form_submission_reject. * The hook fires before Kadence runs the form's own actions (e-mail and so on). The free plugin stores no entries, * so one submission is told apart by a hash of the form, its values, the sender and a half-hour window. */ class KadenceBlocks { const GROUP = 'Kadence Blocks'; const ADVANCED_PREFIX = 'kadence.advanced_form.'; const SIMPLE_EVENT = 'kadence.simple_form'; const POST_TYPE = 'kadence_form'; const OBJECT_TYPE = 'kadence_form'; /** * Advanced Form field blocks that are not variables: no value (captcha, submit) or a link to a file. */ const SKIPPED_TYPES = array( 'submit', 'captcha', 'file' ); /** * How many numbered fields (field_1...) the editor offers for the simple Form block. */ const SIMPLE_FIELDS = 10; /** * Context keys of our own (prefixed so they cannot clash with field variables). */ const KEY_FORM = '_smsportal_form_id'; const KEY_HASH = '_smsportal_submission'; /** * Length of the window in which the same data from the same visitor counts as one submission. */ const DUPLICATE_WINDOW = 1800; /** * Whether Kadence Blocks is active. * * @return bool */ public static function available(): bool { return defined( 'KADENCE_BLOCKS_VERSION' ); } /** * Register hooks. * * @return void */ public static function register(): void { add_filter( 'smsportal_register_events', array( self::class, 'events' ) ); add_action( 'kadence_blocks_advanced_form_submission', array( self::class, 'advanced_submitted' ), 20, 3 ); add_action( 'kadence_blocks_form_submission', array( self::class, 'simple_submitted' ), 20, 4 ); } /** * Add one event per Advanced Form and the one for the simple Form block. * * @param mixed $events Events so far. * @return array> */ public static function events( $events ): array { $events = is_array( $events ) ? $events : array(); $forms = get_posts( array( 'post_type' => self::POST_TYPE, 'post_status' => 'publish', 'posts_per_page' => -1, 'orderby' => 'ID', 'order' => 'ASC', ) ); foreach ( $forms as $form ) { $events[] = self::advanced_event( $form ); } $events[] = self::simple_event(); return $events; } /** * Hook: an Advanced Form was submitted and accepted. * * @param mixed $form_args Form settings and fields. * @param mixed $processed_fields Submitted fields: label, type, value, uniqueID, name. * @param mixed $post_id Id of the form's post. * @return void */ public static function advanced_submitted( $form_args, $processed_fields, $post_id = 0 ): void { $form_id = (int) $post_id; if ( $form_id <= 0 || ! is_array( $processed_fields ) ) { return; } $context = array(); foreach ( $processed_fields as $field ) { $type = is_array( $field ) ? (string) ( $field['type'] ?? '' ) : ''; if ( '' === $type || in_array( $type, self::SKIPPED_TYPES, true ) ) { continue; } $name = self::variable_name( (string) ( $field['name'] ?? '' ), (string) ( $field['uniqueID'] ?? '' ) ); if ( '' !== $name ) { $context[ $name ] = self::value( $field['value'] ?? '', $type ); } } self::fire( self::ADVANCED_PREFIX . $form_id, $form_id, $context ); } /** * Hook: a simple Form block was submitted and accepted. * * @param mixed $form_args Form settings. * @param mixed $fields Submitted fields by their place in the form: type, label, value. * @param mixed $form_id uniqueID of the form block. * @param mixed $post_id Id of the post holding the form (or "block..." for a widget). * @return void */ public static function simple_submitted( $form_args, $fields, $form_id = '', $post_id = '' ): void { if ( ! is_array( $fields ) ) { return; } $context = array(); foreach ( $fields as $index => $field ) { if ( is_array( $field ) && is_numeric( $index ) ) { $context[ 'field_' . ( (int) $index + 1 ) ] = self::value( $field['value'] ?? '', (string) ( $field['type'] ?? '' ) ); } } // The form is told apart by the page it sits on and its own id. self::fire( self::SIMPLE_EVENT, md5( (string) $post_id . '|' . (string) $form_id ), $context ); } /** * Fire the event of a submission. * * @param string $event_id Event id. * @param int|string $form Form identifier (goes into the dedupe hash). * @param array $context Values. * @return void */ private static function fire( string $event_id, $form, array $context ): void { try { $context[ self::KEY_FORM ] = $form; $context[ self::KEY_HASH ] = self::hash( $event_id, $form, $context ); Events::fire( $event_id, $context ); } catch ( \Throwable $e ) { Events::report( $e, $event_id ); } } /** * Event of one Advanced Form. * * @param \WP_Post $form Form. * @return array */ private static function advanced_event( \WP_Post $form ): array { $form_id = (int) $form->ID; return self::base_event( self::ADVANCED_PREFIX . $form_id, /* translators: %s: form title. */ sprintf( __( 'Kadence Blocks: form "%s" sent', '85pl-android-sms-api' ), $form->post_title ), self::advanced_variables( $form ), admin_url( 'post.php?post=' . $form_id . '&action=edit' ) ); } /** * Event of the simple Form block (all such forms). * * @return array */ private static function simple_event(): array { $variables = array(); for ( $n = 1; $n <= self::SIMPLE_FIELDS; $n++ ) { /* translators: %d: place of the field in the form, counted from 1. */ $variables[ 'field_' . $n ] = sprintf( __( 'Form field %d', '85pl-android-sms-api' ), $n ); } return self::base_event( self::SIMPLE_EVENT, __( 'Kadence Blocks: form sent (simple Form block)', '85pl-android-sms-api' ), $variables, admin_url( 'edit.php?post_type=page' ) ); } /** * Parts of the event both kinds share. * * @param string $id Event id. * @param string $label Event label. * @param array $variables Variables. * @param string $url Where the form is edited. * @return array */ private static function base_event( string $id, string $label, array $variables, string $url ): array { return array( 'id' => $id, 'label' => $label, 'group' => self::GROUP, 'icon' => SMSPORTAL_URL_BASE . 'assets/integrations/kadence-blocks.png', 'variables' => $variables, 'customer_phone' => static function ( array $context, array $rule = array() ) { $field = (string) ( $rule['phone_field'] ?? '' ); $phone = '' !== $field ? ( $context[ $field ] ?? null ) : null; return is_string( $phone ) && '' !== trim( $phone ) ? $phone : null; }, 'object' => static function ( array $context ) use ( $id, $url ): array { $hash = (string) ( $context[ self::KEY_HASH ] ?? '' ); return array( 'type' => self::OBJECT_TYPE, // One SMS per submission: the free plugin stores no entry, so a hash of the data and sender. 'id' => '' === $hash ? '' : $id . '-' . $hash, 'url' => $url, ); }, // The free plugin does not store submissions, so there is nothing to preview. 'sample' => null, 'rule_fields' => array( 'phone_field' ), ); } /** * Variables of an Advanced Form: variable name => the field's label (the phone field can be any of them). * * @param \WP_Post $form Form. * @return array */ private static function advanced_variables( \WP_Post $form ): array { $variables = array(); $position = 0; foreach ( self::field_blocks( parse_blocks( $form->post_content ) ) as $block ) { $attrs = is_array( $block['attrs'] ?? null ) ? $block['attrs'] : array(); $type = substr( (string) $block['blockName'], strlen( 'kadence/advanced-form-' ) ); ++$position; if ( in_array( $type, self::SKIPPED_TYPES, true ) ) { continue; } $unique_id = (string) ( $attrs['uniqueID'] ?? '' ); $raw = ! empty( $attrs['inputName'] ) ? (string) $attrs['inputName'] : 'field' . $unique_id; $name = self::variable_name( $raw, $unique_id ); if ( '' === $name ) { continue; } $label = trim( wp_strip_all_tags( (string) ( $attrs['label'] ?? '' ) ) ); if ( '' === $label ) { /* translators: %d: field number in the form. */ $label = sprintf( __( 'Field %d', '85pl-android-sms-api' ), $position ); } $variables[ $name ] = $label; } return $variables; } /** * Field blocks of an Advanced Form in the order Kadence reads them (it goes into nested blocks, and takes a block * without inner blocks as a field). * * @param array $blocks Parsed blocks. * @return array> */ private static function field_blocks( array $blocks ): array { $found = array(); foreach ( $blocks as $block ) { if ( ! is_array( $block ) || ! isset( $block['blockName'] ) ) { continue; } if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) { $found = array_merge( $found, self::field_blocks( $block['innerBlocks'] ) ); } elseif ( 0 === strpos( (string) $block['blockName'], 'kadence/advanced-form-' ) && 'kadence/advanced-form-submit' !== $block['blockName'] ) { $found[] = $block; } } return $found; } /** * A name usable as a template variable: the field's name when it has only A-Z, a-z, 0-9 and _:.- in it, * otherwise field_ (cleaned the same way). * * @param string $name The name Kadence uses for the field (its "name" setting, or field). * @param string $unique_id The field's unique id. * @return string Empty when no safe name can be made. */ private static function variable_name( string $name, string $unique_id ): string { if ( '' !== $name && 1 === preg_match( '/^[A-Za-z0-9_:.\-]+$/', $name ) ) { return $name; } $clean = (string) preg_replace( '/[^A-Za-z0-9_:.\-]/', '', $unique_id ); return '' === $clean ? '' : 'field_' . $clean; } /** * One value as text: several lines become a comma-separated list (not for a message field); capped in length. * * @param mixed $value Value from Kadence. * @param string $type Field type. * @return string */ private static function value( $value, string $type ): string { $text = is_scalar( $value ) ? (string) $value : ''; if ( 'textarea' !== $type ) { $text = (string) preg_replace( '/\s*\R\s*/', ', ', trim( $text ) ); } return $text; } /** * Identifier of a submission (Kadence stores none): the form, the data, the sender and a half-hour window. * * @param string $event_id Event id. * @param int|string $form Form identifier. * @param array $context Values. * @return string */ private static function hash( string $event_id, $form, array $context ): string { $sender = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : ''; $window = (int) floor( time() / self::DUPLICATE_WINDOW ); return md5( wp_json_encode( array( $event_id, $form, $context, $sender, $window ) ) ); } }