get( 'fields' )` * is keyed by each field's ID, each with its `value`. * - Which form it was: the form widget's element id, from the record's * form settings (`id`), or the `form_id` Elementor posts with every * submission. * - Listing forms: Elementor keeps each page's design in the * `_elementor_data` post meta (JSON). Form widgets (`widgetType: form`) * are found there, with their `form_name` and `form_fields` - each field's * `custom_id`, `field_type` (text when not set), `field_label` and, for an * acceptance box, `acceptance_text`. * * @package Emails404 */ namespace Emails404\Providers; if ( ! defined( 'ABSPATH' ) ) { exit; } use Emails404\Form_Connections; use Emails404\Form_Source_Interface; class Elementor_Forms_Provider implements Form_Source_Interface { const SLUG = 'elementor'; /** Pages scanned for forms - plenty for a site's real forms, and keeps the screen quick. */ const MAX_PAGES = 300; /** @var array|null */ private $forms = null; public function get_slug() { return self::SLUG; } public function get_label() { return __( 'Elementor forms', '404-emails' ); } public function is_available() { return defined( 'ELEMENTOR_PRO_VERSION' ); } public function register_hooks() { add_action( 'elementor_pro/forms/new_record', array( $this, 'handle_record' ), 10, 2 ); } public function list_forms() { if ( null !== $this->forms ) { return $this->forms; } global $wpdb; $this->forms = array(); if ( ! $this->is_available() ) { return $this->forms; } // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- Elementor stores forms only inside this meta; read-only, on an admin screen. $rows = $wpdb->get_results( $wpdb->prepare( "SELECT pm.post_id, pm.meta_value, p.post_title FROM {$wpdb->postmeta} pm INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id WHERE pm.meta_key = '_elementor_data' AND pm.meta_value LIKE %s AND p.post_status IN ('publish', 'private', 'draft', 'future') AND p.post_type <> 'revision' ORDER BY pm.post_id DESC LIMIT %d", '%' . $wpdb->esc_like( '"widgetType":"form"' ) . '%', self::MAX_PAGES ) ); $seen = array(); foreach ( is_array( $rows ) ? $rows : array() as $row ) { foreach ( self::forms_in_page_data( (string) $row->meta_value, (string) $row->post_title ) as $form ) { if ( ! isset( $seen[ $form['id'] ] ) ) { $seen[ $form['id'] ] = true; $this->forms[] = $form; } } } return $this->forms; } public function describe_form( $form_id ) { foreach ( $this->list_forms() as $form ) { if ( (string) $form_id === $form['id'] ) { return $form; } } return null; } /** * @param object $record ElementorPro\Modules\Forms\Classes\Form_Record. * @param object $handler The Ajax handler (not used). */ public function handle_record( $record, $handler = null ) { if ( ! is_object( $record ) || ! method_exists( $record, 'get' ) ) { return; } $form_id = self::form_id_of( $record ); if ( '' === $form_id || null === Form_Connections::get( self::SLUG, $form_id ) ) { return; } $values = array(); $fields = $record->get( 'fields' ); foreach ( is_array( $fields ) ? $fields : array() as $id => $field ) { $values[ (string) $id ] = is_array( $field ) && isset( $field['value'] ) ? $field['value'] : ''; } Form_Connections::receive( $this, $form_id, $values, wp_generate_uuid4() ); } private static function form_id_of( $record ) { $settings = $record->get( 'form_settings' ); if ( is_array( $settings ) && ! empty( $settings['id'] ) ) { return sanitize_key( (string) $settings['id'] ); } // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Elementor has already checked this submission; only which form it was is read. return isset( $_POST['form_id'] ) ? sanitize_key( wp_unslash( $_POST['form_id'] ) ) : ''; } /** * Form widgets in one page's `_elementor_data`. * * @param string $json The meta value. * @param string $page_title Shown after the form's name, as forms are often all called "New Form". * @return array */ public static function forms_in_page_data( $json, $page_title ) { $elements = json_decode( $json, true ); $forms = array(); if ( is_array( $elements ) ) { self::collect( $elements, $page_title, $forms ); } return $forms; } private static function collect( array $elements, $page_title, array &$forms ) { foreach ( $elements as $element ) { if ( ! is_array( $element ) ) { continue; } if ( isset( $element['widgetType'], $element['id'] ) && 'form' === $element['widgetType'] ) { $settings = isset( $element['settings'] ) && is_array( $element['settings'] ) ? $element['settings'] : array(); $name = isset( $settings['form_name'] ) && '' !== trim( (string) $settings['form_name'] ) ? (string) $settings['form_name'] : __( 'Form', '404-emails' ); $forms[] = array( 'id' => sanitize_key( (string) $element['id'] ), 'title' => '' !== $page_title ? $name . ' – ' . $page_title : $name, 'fields' => self::fields_from_settings( isset( $settings['form_fields'] ) && is_array( $settings['form_fields'] ) ? $settings['form_fields'] : array() ), ); } if ( isset( $element['elements'] ) && is_array( $element['elements'] ) ) { self::collect( $element['elements'], $page_title, $forms ); } } } public static function fields_from_settings( array $form_fields ) { $fields = array(); foreach ( $form_fields as $field ) { if ( ! is_array( $field ) || empty( $field['custom_id'] ) ) { continue; } $key = (string) $field['custom_id']; $type = isset( $field['field_type'] ) && '' !== $field['field_type'] ? (string) $field['field_type'] : 'text'; $label = isset( $field['field_label'] ) ? (string) $field['field_label'] : ''; if ( 'email' === $type ) { $fields[] = array( 'key' => $key, 'label' => $label, 'kind' => Form_Connections::KIND_EMAIL ); } elseif ( 'acceptance' === $type ) { $text = isset( $field['acceptance_text'] ) && '' !== trim( (string) $field['acceptance_text'] ) ? (string) $field['acceptance_text'] : $label; $fields[] = array( 'key' => $key, 'label' => trim( wp_strip_all_tags( $text ) ), 'kind' => Form_Connections::KIND_CONSENT ); } elseif ( 'checkbox' === $type ) { $options = isset( $field['field_options'] ) ? array_values( array_filter( array_map( 'trim', explode( "\n", (string) $field['field_options'] ) ) ) ) : array(); $option = 1 === count( $options ) ? trim( explode( '|', $options[0] )[0] ) : ''; $fields[] = array( 'key' => $key, 'label' => '' !== $option ? $option : $label, 'kind' => Form_Connections::KIND_CONSENT ); } elseif ( 'text' === $type ) { $fields[] = array( 'key' => $key, 'label' => $label, 'kind' => Form_Connections::kind_from_words( $key, $label ) ); } } return $fields; } }