'text', 'input_mask' => 'text', 'input_email' => 'email', 'textarea' => 'textarea', 'rich_text_input' => 'textarea', 'input_number' => 'number', 'rangeslider' => 'number', 'net_promoter_score' => 'number', 'ratings' => 'number', 'select' => 'select', 'select_country' => 'select', 'cpt_selection' => 'select', 'input_radio' => 'radio', 'input_checkbox' => 'checkbox', 'gdpr_agreement' => 'checkbox', 'terms_and_condition' => 'checkbox', 'input_url' => 'url', 'input_date' => 'date', 'phone' => 'tel', 'input_hidden' => 'hidden', 'color-picker' => 'text', 'color_picker' => 'text', 'input_ranking' => 'other', ); /** * {@inheritDoc} */ public function slug() { return 'fluent-forms'; } /** * {@inheritDoc} */ public function name() { return 'Fluent Forms'; } /** * {@inheritDoc} */ public function is_available() { return defined( 'FLUENTFORM' ) || defined( 'FLUENTFORM_VERSION' ); } /** * {@inheritDoc} */ public function forms() { global $wpdb; $rows = $wpdb->get_results( $wpdb->prepare( "SELECT id, title FROM {$wpdb->prefix}fluentform_forms ORDER BY title ASC LIMIT %d", self::MAX_FORMS ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $forms = array(); foreach ( (array) $rows as $row ) { $forms[] = array( 'id' => (string) $row->id, 'title' => self::plain( $row->title ), ); } return $forms; } /** * {@inheritDoc} */ public function fields( $form_id ) { global $wpdb; if ( ! (int) $form_id ) { return array(); } $json = $wpdb->get_var( $wpdb->prepare( "SELECT form_fields FROM {$wpdb->prefix}fluentform_forms WHERE id = %d", (int) $form_id ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $fields = array(); foreach ( $this->definitions( $json ) as $definition ) { $fields[] = array( 'name' => $definition['name'], 'label' => $definition['label'], 'type' => $definition['type'], ); } return $fields; } /** * {@inheritDoc} */ public function register() { if ( ! $this->claim( 'hooks' ) ) { return; } /* * `fluentform/submission_inserted` ($insertId, $formData, $form), fired by * SubmissionHandlerService::processSubmissionData() once FormValidationService::validateSubmission() * passed (nonce, reCAPTCHA, hCaptcha, Turnstile, the field rules) and the submission row was * inserted; every submission path goes through handleSubmission(). Fluent Forms 5.0 renamed it * from `fluentform_submission_inserted` and still fires both, back to back, so only one is added: * the legacy name only on a version older than 5.0. When Akismet or CleanTalk flags a submission * without "skip processing", the hook still fires with the row stored as `spam`, so handle() * reads the row's status. */ $legacy = defined( 'FLUENTFORM_VERSION' ) && version_compare( FLUENTFORM_VERSION, '5.0.0', '<' ); add_action( $legacy ? 'fluentform_submission_inserted' : 'fluentform/submission_inserted', array( $this, 'handle' ), 20, 3 ); } /** * Relay one inserted Fluent Forms submission. * * @param int $insert_id The submission's ID. * @param array $form_data The submitted values, keyed by input name. * @param object $form The form (`id`, `title`, `form_fields` JSON). * @return void */ public function handle( $insert_id, $form_data, $form ) { global $wpdb; try { if ( ! is_array( $form_data ) || ! is_object( $form ) || empty( $form->id ) ) { return; } $form_id = (string) $form->id; $key = $form_id . ':' . ( $insert_id ? (string) $insert_id : md5( serialize( $form_data ) ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize if ( ! $this->claim( 'submission:' . $key ) ) { return; } if ( $insert_id ) { $status = $wpdb->get_var( $wpdb->prepare( "SELECT status FROM {$wpdb->prefix}fluentform_submissions WHERE id = %d", (int) $insert_id ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching if ( 'spam' === $status ) { return; } } $values = array(); foreach ( $this->definitions( isset( $form->form_fields ) ? $form->form_fields : '' ) as $definition ) { $value = isset( $form_data[ $definition['key'] ] ) ? $form_data[ $definition['key'] ] : ''; if ( '' !== $definition['part'] ) { $value = is_array( $value ) && isset( $value[ $definition['part'] ] ) ? $value[ $definition['part'] ] : ''; } self::push( $values, $definition['name'], $definition['label'], $definition['type'], $value ); } $this->send( $form_id, isset( $form->title ) ? self::plain( $form->title ) : '', $values ); } catch ( \Throwable $e ) { $this->fail( $e ); } } /** * The answer fields of a form, in form order, from its `form_fields` JSON. * * @param mixed $json The form's `form_fields`. * @return array */ private function definitions( $json ) { $structure = is_string( $json ) ? json_decode( $json, true ) : ( is_array( $json ) ? $json : null ); if ( ! is_array( $structure ) || empty( $structure['fields'] ) || ! is_array( $structure['fields'] ) ) { return array(); } $definitions = array(); $this->walk( $structure['fields'], $definitions, 0 ); return $definitions; } /** * Collect answer fields from a list of elements, entering containers' columns. * * @param array $elements Elements. * @param array $definitions Collected answers. * @param int $depth Current depth, to stop runaway trees. * @return void */ private function walk( array $elements, array &$definitions, $depth ) { if ( $depth > 10 ) { return; } foreach ( $elements as $element ) { if ( ! is_array( $element ) || empty( $element['element'] ) ) { continue; } $type = (string) $element['element']; // Containers (one to six columns) and anything else laid out in columns. if ( ! empty( $element['columns'] ) && is_array( $element['columns'] ) ) { foreach ( $element['columns'] as $column ) { if ( is_array( $column ) && ! empty( $column['fields'] ) && is_array( $column['fields'] ) ) { $this->walk( $column['fields'], $definitions, $depth + 1 ); } } continue; } $key = isset( $element['attributes']['name'] ) ? (string) $element['attributes']['name'] : ''; if ( '' === $key ) { continue; } $label = $this->label( $element, $key ); if ( in_array( $type, self::COMPOSITE, true ) ) { foreach ( ( isset( $element['fields'] ) && is_array( $element['fields'] ) ? $element['fields'] : array() ) as $part_key => $part ) { if ( ! is_array( $part ) || ( isset( $part['settings']['visible'] ) && ! $part['settings']['visible'] ) ) { continue; } $part_name = isset( $part['attributes']['name'] ) ? (string) $part['attributes']['name'] : (string) $part_key; $part_type = isset( $part['element'] ) ? (string) $part['element'] : 'input_text'; $definitions[] = array( 'name' => $key . '.' . $part_name, 'label' => self::part_label( $label, $this->label( $part, $part_name ) ), 'type' => isset( self::TYPE_MAP[ $part_type ] ) ? self::TYPE_MAP[ $part_type ] : 'text', 'key' => $key, 'part' => $part_name, ); } continue; } if ( ! isset( self::TYPE_MAP[ $type ] ) ) { continue; } $definitions[] = array( 'name' => $key, 'label' => $label, 'type' => self::TYPE_MAP[ $type ], 'key' => $key, 'part' => '', ); } } /** * An element's label: its label, else its admin label, else its name. * * @param array $element The element. * @param string $name Its input name. * @return string */ private function label( array $element, $name ) { foreach ( array( 'label', 'admin_field_label' ) as $setting ) { if ( isset( $element['settings'][ $setting ] ) ) { $label = self::plain( $element['settings'][ $setting ] ); if ( '' !== $label ) { return $label; } } } return (string) $name; } }