_ (names_first_name, address_1_city). */ class FluentForms { const GROUP = 'Fluent Forms'; const EVENT_PREFIX = 'fluentform.submitted.'; const OBJECT_TYPE = 'fluentform_entry'; /** * Element types that are not template variables. Fluent Forms only parses real inputs (no HTML, steps, section * breaks); of those, these have sensitive or binary values (password, files), many values (repeater, * checkable grid) or only "on" (consent checkboxes). */ const SKIPPED_ELEMENTS = array( 'input_password', 'input_file', 'input_image', 'input_repeat', 'repeater_field', 'tabular_grid', 'terms_and_condition', 'gdpr_agreement', ); /** * Context keys of our own (prefixed so they cannot clash with field variables). */ const KEY_FORM = '_smsportal_form_id'; const KEY_ENTRY = '_smsportal_entry_id'; /** * Whether Fluent Forms is active. * * @return bool */ public static function available(): bool { return defined( 'FLUENTFORM' ) && class_exists( '\FluentForm\App\Models\Form' ); } /** * Register hooks. * * @return void */ public static function register(): void { add_filter( 'smsportal_register_events', array( self::class, 'events' ) ); add_action( 'fluentform/submission_inserted', array( self::class, 'submitted' ), 20, 3 ); } /** * Add one event per published form. * * @param mixed $events Events so far. * @return array> */ public static function events( $events ): array { $events = is_array( $events ) ? $events : array(); $forms = \FluentForm\App\Models\Form::where( 'status', 'published' )->orderBy( 'id', 'ASC' )->get(); foreach ( $forms as $form ) { $events[] = self::event( $form ); } return $events; } /** * Hook: an entry was stored (Fluent Forms does not call it for failed submissions). * * @param mixed $insert_id Entry id. * @param mixed $data Submitted values, by field name. * @param mixed $form Form. * @return void */ public static function submitted( $insert_id, $data, $form ): void { $entry_id = (int) $insert_id; $form_id = is_object( $form ) ? (int) ( $form->id ?? 0 ) : 0; if ( $entry_id <= 0 || $form_id <= 0 || ! is_array( $data ) ) { return; } $event_id = self::EVENT_PREFIX . $form_id; try { // An entry that the spam check kept ("mark as spam and continue") still runs the actions. $entry = \FluentForm\App\Models\Submission::find( $entry_id ); if ( $entry && 'spam' === $entry->status ) { return; } Events::fire( $event_id, self::context( $form, $data, $entry_id ) ); } catch ( \Throwable $e ) { Events::report( $e, $event_id ); } } /** * Event of one form. * * @param object $form Form row. * @return array */ private static function event( $form ): array { $form_id = (int) $form->id; $labels = array(); foreach ( self::fields( $form ) as $name => $field ) { $labels[ $name ] = $field['label']; } return array( 'id' => self::EVENT_PREFIX . $form_id, /* translators: %s: form title. */ 'label' => sprintf( __( 'Fluent Forms: form "%s" sent', '85pl-android-sms-api' ), $form->title ), 'group' => self::GROUP, 'icon' => SMSPORTAL_URL_BASE . 'assets/integrations/fluent-forms.png', 'variables' => $labels, '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 { $entry_id = (int) ( $context[ self::KEY_ENTRY ] ?? 0 ); return array( 'type' => self::OBJECT_TYPE, // One SMS per entry, however many times Fluent Forms runs its actions. 'id' => $entry_id > 0 ? $form_id . '-' . $entry_id : '', 'url' => admin_url( 'admin.php?page=fluent_forms&route=entries&form_id=' . $form_id . '#/entries/' . $entry_id ), ); }, // Entries are stored by Fluent Forms, but a sample would show private data; no preview. 'sample' => null, 'rule_fields' => array( 'phone_field' ), ); } /** * Fields of a form that become variables: variable name => label, element, option labels and where the value * sits in the submitted data (name, or name and part). * * @param object $form Form row (needs form_fields). * @return array> */ private static function fields( $form ): array { $parser = new \FluentForm\App\Services\Parser\Form( $form ); $inputs = $parser->getInputs( array( 'admin_label', 'element', 'options', 'raw' ) ); $fields = array(); foreach ( $inputs as $key => $input ) { $element = (string) ( $input['element'] ?? '' ); if ( in_array( $element, self::SKIPPED_ELEMENTS, true ) || false !== strpos( (string) $key, '*' ) ) { continue; } $path = array( (string) $key ); $label = trim( (string) ( $input['admin_label'] ?? '' ) ); if ( preg_match( '/^([^\[\]]+)\[([^\[\]]+)\]$/', (string) $key, $match ) ) { // A part of a name or address field: label it with the parent's label and the part's own label. $parent_label = isset( $inputs[ $match[1] ]['admin_label'] ) ? (string) $inputs[ $match[1] ]['admin_label'] : $match[1]; $part_label = (string) ( $inputs[ $match[1] ]['raw']['fields'][ $match[2] ]['settings']['label'] ?? $match[2] ); $label = sprintf( '%1$s (%2$s)', $parent_label, $part_label ); $path = array( $match[1], $match[2] ); } $name = self::variable_name( (string) $key, count( $fields ) + 1 ); if ( isset( $fields[ $name ] ) ) { continue; } $fields[ $name ] = array( 'label' => '' !== $label ? $label : $name, 'element' => $element, 'options' => is_array( $input['options'] ?? null ) ? $input['options'] : array(), 'path' => $path, ); } return $fields; } /** * Variable name of a field: its name, with brackets (name parts) turned into "_"; a name with other unusable * characters gets them replaced. * * @param string $key Field name, e.g. email or names[first_name]. * @param int $index Position of the field, used when nothing usable is left. * @return string */ private static function variable_name( string $key, int $index ): string { $name = trim( (string) preg_replace( '/[^A-Za-z0-9_:.\-]+/', '_', $key ), '_' ); return '' !== $name ? $name : 'field_' . $index; } /** * Submitted values as template variables (lists joined with commas). * * @param object $form Form. * @param array $data Submitted values. * @param int $entry_id Entry id. * @return array */ private static function context( $form, array $data, int $entry_id ): array { $context = array(); foreach ( self::fields( $form ) as $name => $field ) { $value = $data[ $field['path'][0] ] ?? ''; if ( isset( $field['path'][1] ) ) { $value = is_array( $value ) ? ( $value[ $field['path'][1] ] ?? '' ) : ''; } $context[ $name ] = self::value( $value, $field ); } $context[ self::KEY_FORM ] = (int) $form->id; $context[ self::KEY_ENTRY ] = $entry_id; return $context; } /** * One value as text: choices become their labels, a list or a name/address becomes a comma-separated text * (a name with spaces); capped in length. * * @param mixed $value Value from Fluent Forms. * @param array $field Field (element and options). * @return string */ private static function value( $value, array $field ): string { $values = array(); foreach ( is_array( $value ) ? self::flatten( $value ) : array( $value ) as $item ) { $item = is_scalar( $item ) ? trim( (string) $item ) : ''; if ( '' !== $item ) { $values[] = $field['options'][ $item ] ?? $item; } } $text = implode( 'input_name' === $field['element'] ? ' ' : ', ', $values ); if ( 'textarea' !== $field['element'] ) { $text = (string) preg_replace( '/\s*\R\s*/', ', ', $text ); } return $text; } /** * Values of a nested array, in order. * * @param array $values Values. * @return array */ private static function flatten( array $values ): array { $flat = array(); foreach ( $values as $value ) { if ( is_array( $value ) ) { $flat = array_merge( $flat, self::flatten( $value ) ); } else { $flat[] = $value; } } return $flat; } }