). * * @package Smsportal */ namespace Smsportal\Integrations; use Smsportal\Events; defined( 'ABSPATH' ) || exit; /** * A sent form as an SMS event. The customer's number comes from the field chosen in the rule * (rule field phone_field); a "customer" rule without that choice sends nothing. * * WPForms field ids are numbers, so a field is the variable field_ (the editor shows its label); * a name field in the first/last format also gives field__first and field__last. */ class WPForms { const GROUP = 'WPForms'; const EVENT_PREFIX = 'wpforms.submitted.'; const OBJECT_TYPE = 'wpforms_form'; const POST_TYPE = 'wpforms'; /** * Field types that are not template variables: display only (no value) or sensitive/binary (card data, * passwords, files, signatures, captchas). */ const SKIPPED_TYPES = array( 'html', 'content', 'divider', 'pagebreak', 'layout', 'repeater', 'internal-information', 'entry-preview', 'file-upload', 'password', 'signature', 'camera', 'credit-card', 'stripe-credit-card', 'square', 'authorize_net', 'captcha', 'custom-captcha', 'recaptcha', 'hcaptcha', 'turnstile', ); /** * 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 (Lite stores no entries). */ const DUPLICATE_WINDOW = 1800; /** * Whether WPForms is active. * * @return bool */ public static function available(): bool { return function_exists( 'wpforms' ) && function_exists( 'wpforms_decode' ); } /** * Register hooks. * * @return void */ public static function register(): void { add_filter( 'smsportal_register_events', array( self::class, 'events' ) ); add_action( 'wpforms_process_complete', array( self::class, 'submitted' ), 20, 4 ); } /** * Add one event per form. * * @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::event( $form ); } return $events; } /** * Hook: a form was submitted and accepted (WPForms does not call it for failed or spam submissions). * * @param mixed $fields Processed fields, by field id. * @param mixed $entry Raw submission. * @param mixed $form_data Form definition. * @param mixed $entry_id Entry id (0 in Lite, which stores no entries). * @return void */ public static function submitted( $fields, $entry, $form_data, $entry_id = 0 ): void { $form_id = is_array( $form_data ) ? (int) ( $form_data['id'] ?? 0 ) : 0; if ( $form_id <= 0 || ! is_array( $fields ) ) { return; } $event_id = self::EVENT_PREFIX . $form_id; try { Events::fire( $event_id, self::context( $form_id, $fields, (int) $entry_id ) ); } catch ( \Throwable $e ) { Events::report( $e, $event_id ); } } /** * Event of one form. * * @param \WP_Post $form Form. * @return array */ private static function event( \WP_Post $form ): array { $form_id = (int) $form->ID; return array( 'id' => self::EVENT_PREFIX . $form_id, /* translators: %s: form title. */ 'label' => sprintf( __( 'WPForms: form "%s" sent', '85pl-android-sms-api' ), $form->post_title ), 'group' => self::GROUP, 'icon' => SMSPORTAL_URL_BASE . 'assets/integrations/wpforms.png', 'variables' => self::variables( $form ), '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 ( $form_id ): array { $id = (string) ( $context[ self::KEY_HASH ] ?? '' ); return array( 'type' => self::OBJECT_TYPE, // One SMS per submission: the entry id, or (Lite stores none) a hash of the data and sender. 'id' => '' === $id ? '' : $form_id . '-' . $id, 'url' => admin_url( 'admin.php?page=wpforms-builder&view=fields&form_id=' . $form_id ), ); }, // WPForms Lite does not store submissions, so there is nothing to preview. 'sample' => null, 'rule_fields' => array( 'phone_field' ), ); } /** * Variables of a 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 variables( \WP_Post $form ): array { $data = wpforms_decode( $form->post_content ); $variables = array(); foreach ( is_array( $data ) ? (array) ( $data['fields'] ?? array() ) : array() as $field ) { $type = (string) ( $field['type'] ?? '' ); $id = (int) ( $field['id'] ?? 0 ); if ( $id <= 0 || '' === $type || in_array( $type, self::SKIPPED_TYPES, true ) ) { continue; } $label = trim( (string) ( $field['label'] ?? '' ) ); if ( '' === $label ) { /* translators: %d: field number in the form. */ $label = sprintf( __( 'Field %d', '85pl-android-sms-api' ), $id ); } $variables[ self::name( $id ) ] = $label; if ( 'name' === $type && 'simple' !== ( $field['format'] ?? 'first-last' ) ) { /* translators: %s: label of a name field. */ $variables[ self::name( $id, 'first' ) ] = sprintf( __( '%s (first name)', '85pl-android-sms-api' ), $label ); /* translators: %s: label of a name field. */ $variables[ self::name( $id, 'last' ) ] = sprintf( __( '%s (last name)', '85pl-android-sms-api' ), $label ); } } return $variables; } /** * Variable name of a field (or of a part of it). * * @param int $id Field id. * @param string $part Part: "first", "last" or "" for the whole value. * @return string */ private static function name( int $id, string $part = '' ): string { return 'field_' . $id . ( '' === $part ? '' : '_' . $part ); } /** * Submitted values as template variables (lists joined with commas). * * @param int $form_id Form id. * @param array $fields Processed fields. * @param int $entry_id Entry id (0 when none is stored). * @return array */ private static function context( int $form_id, array $fields, int $entry_id ): array { $context = array(); foreach ( $fields as $field ) { $type = is_array( $field ) ? (string) ( $field['type'] ?? '' ) : ''; $id = is_array( $field ) ? (int) ( $field['id'] ?? 0 ) : 0; if ( $id <= 0 || '' === $type || in_array( $type, self::SKIPPED_TYPES, true ) ) { continue; } $context[ self::name( $id ) ] = self::value( $field['value'] ?? '', $type ); if ( 'name' === $type ) { $context[ self::name( $id, 'first' ) ] = self::value( $field['first'] ?? '', $type ); $context[ self::name( $id, 'last' ) ] = self::value( $field['last'] ?? '', $type ); } } $context[ self::KEY_FORM ] = $form_id; $context[ self::KEY_HASH ] = $entry_id > 0 ? (string) $entry_id : self::hash( $form_id, $context ); return $context; } /** * One value as text: several lines (checkboxes, address) become a comma-separated list; capped in length. * * @param mixed $value Value from WPForms. * @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 when WPForms stores none: the data, the sender and a half-hour window. * * @param int $form_id Form id. * @param array $context Values. * @return string */ private static function hash( int $form_id, 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( $form_id, $context, $sender, $window ) ) ); } }