__('Single line', 'ai-eshop-optimizer'), 'textarea' => __('Paragraph', 'ai-eshop-optimizer'), 'radio' => __('Choose one', 'ai-eshop-optimizer'), 'checkbox' => __('Choose any', 'ai-eshop-optimizer'), 'select' => __('Drop-down', 'ai-eshop-optimizer'), 'email' => __('E-mail', 'ai-eshop-optimizer'), 'url' => __('Web address', 'ai-eshop-optimizer'), 'tel' => __('Telephone', 'ai-eshop-optimizer'), 'consent' => __('Consent', 'ai-eshop-optimizer'), ); } public static function is_choice_type($type) { return in_array($type, array('radio', 'checkbox', 'select'), true); } public static function is_multi_type($type) { return 'checkbox' === $type; } public static function max_length($type) { switch ((string) $type) { case 'textarea': return 5000; case 'email': return 254; case 'url': return 500; case 'tel': return 40; default: return 200; } } const WIDTHS = array('full', 'half'); const PROPS = array('placeholder', 'help', 'missing', 'invalid', 'autocomplete', 'prompt', 'rows'); const MORE = array('help', 'missing', 'invalid', 'autocomplete', 'prompt', 'rows'); public static function normalise_field(array $f, $sanitise = false) { $types = self::field_types(); $type = isset($f['type']) ? (string) $f['type'] : ''; $key = isset($f['key']) ? sanitize_key((string) $f['key']) : ''; if ('' === $key || !isset($types[$type])) { return null; } $text = static function ($v) use ($sanitise) { $v = is_scalar($v) ? (string) $v : ''; return $sanitise ? sanitize_text_field($v) : $v; }; $field = array( 'key' => $key, 'type' => $type, 'label' => array_key_exists('label', $f) ? $text($f['label']) : $key, 'required' => !empty($f['required']), 'choices' => array(), 'width' => isset($f['width']) && in_array((string) $f['width'], self::WIDTHS, true) ? (string) $f['width'] : 'full', ); foreach (self::PROPS as $prop) { $field[$prop] = 'rows' === $prop ? max(0, min(30, (int) ($f['rows'] ?? 0))) : $text($f[$prop] ?? ''); } $field['autocomplete'] = (string) preg_replace('/[^a-z0-9 -]/', '', strtolower($field['autocomplete'])); if (self::is_choice_type($type)) { $choices = isset($f['choices']) ? $f['choices'] : array(); if (is_string($choices)) { $choices = preg_split('/\r\n|\r|\n/', $choices); } foreach ((array) $choices as $k => $c) { if (is_array($c)) { $ck = isset($c['key']) ? (string) $c['key'] : ''; $cl = isset($c['label']) ? (string) $c['label'] : $ck; } elseif (is_string($k)) { $ck = $k; $cl = (string) $c; } elseif (false !== strpos((string) $c, '|')) { list($ck, $cl) = array_map('trim', explode('|', (string) $c, 2)); } else { $ck = (string) $c; $cl = (string) $c; } $ck = sanitize_key($ck); if ('' === $ck || '' === trim($cl)) { continue; } $field['choices'][$ck] = $text($cl); } if (empty($field['choices'])) { return null; } } return $field; } public static function settings($form_id) { $raw = get_post_meta((int) $form_id, self::META_SETTINGS, true); $arr = is_string($raw) ? json_decode($raw, true) : $raw; $arr = is_array($arr) ? $arr : array(); $out = array(); foreach (self::setting_defaults() as $k => $default) { $v = isset($arr[$k]) && is_scalar($arr[$k]) ? trim((string) $arr[$k]) : ''; $out[$k] = '' !== $v ? $v : $default; } return $out; } public static function setting_defaults() { return array( 'button' => __('Send', 'ai-eshop-optimizer'), 'sending' => __('Sending…', 'ai-eshop-optimizer'), 'success' => __('Thank you. We have your message and will be in touch shortly.', 'ai-eshop-optimizer'), 'check' => __('Please check the fields marked above.', 'ai-eshop-optimizer'), 'notify_to' => '', 'subject' => '', 'ack_subject' => '', 'ack_body' => '', 'cta_label' => '', 'cta_opens' => 'modal', 'cta_page' => '', 'cta_contact' => '', 'cta_location' => '', 'cta_icons' => '', ); } const TRANSLATABLE = array('cta_label', 'button', 'sending', 'success', 'check'); const STRINGS_DOMAIN = 'Rooster One forms'; public static function words($form_id) { $s = self::settings($form_id); $raw = get_post_meta((int) $form_id, self::META_SETTINGS, true); $own = is_string($raw) ? json_decode($raw, true) : array(); $own = is_array($own) ? $own : array(); foreach (self::TRANSLATABLE as $k) { $v = isset($own[$k]) && is_scalar($own[$k]) ? trim((string) $own[$k]) : ''; if ('' === $v) { continue; } if (has_filter('wpml_translate_single_string')) { $s[$k] = (string) apply_filters('wpml_translate_single_string', $v, self::STRINGS_DOMAIN, 'form ' . (int) $form_id . ': ' . $k); } elseif (function_exists('pll__')) { $s[$k] = (string) pll__($v); } } return $s; } public static function register_strings($form_id) { $raw = get_post_meta((int) $form_id, self::META_SETTINGS, true); $own = is_string($raw) ? json_decode($raw, true) : array(); $own = is_array($own) ? $own : array(); foreach (self::TRANSLATABLE as $k) { $v = isset($own[$k]) && is_scalar($own[$k]) ? trim((string) $own[$k]) : ''; if ('' !== $v) { do_action('wpml_register_single_string', self::STRINGS_DOMAIN, 'form ' . (int) $form_id . ': ' . $k, $v); } } } public static function register_polylang_strings() { if (!function_exists('pll_register_string') || !isset($_GET['page']) || 'mlang_strings' !== sanitize_key(wp_unslash($_GET['page']))) { return; } foreach (get_posts(array('post_type' => self::POST_TYPE, 'post_status' => 'publish', 'numberposts' => 200, 'fields' => 'ids', 'no_found_rows' => true)) as $id) { $raw = get_post_meta((int) $id, self::META_SETTINGS, true); $own = is_string($raw) ? json_decode($raw, true) : array(); foreach (self::TRANSLATABLE as $k) { $v = is_array($own) && isset($own[$k]) && is_scalar($own[$k]) ? trim((string) $own[$k]) : ''; if ('' !== $v) { pll_register_string('form ' . (int) $id . ': ' . $k, $v, self::STRINGS_DOMAIN); } } } } public static function save_definition($post_id, array $fields, array $settings = array(), $notify = null) { $post_id = (int) $post_id; $clean = self::clean_fields($fields); $json = (string) wp_json_encode($clean, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); $changed = false; if ((string) get_post_meta($post_id, self::META_FIELDS, true) !== $json) { update_post_meta($post_id, self::META_FIELDS, wp_slash($json)); $changed = true; } $kept = array(); $defaults = self::setting_defaults(); foreach (array_keys($defaults) as $k) { $v = isset($settings[$k]) && is_scalar($settings[$k]) ? (string) $settings[$k] : ''; $v = in_array($k, array('ack_body'), true) ? sanitize_textarea_field($v) : sanitize_text_field($v); if ('cta_page' === $k) { $v = absint($v) > 0 ? (string) absint($v) : ''; } elseif ('cta_opens' === $k) { $v = 'page' === $v ? 'page' : ''; } elseif ('cta_contact' === $k) { $v = class_exists('AIEO_Location_Contact') ? implode(',', AIEO_Location_Contact::parse($v)) : ''; } elseif ('cta_location' === $k) { $v = sanitize_key($v); } elseif ('cta_icons' === $k) { $v = 'hide' === $v ? 'hide' : ''; } if ('' !== trim($v)) { $kept[$k] = trim($v); } } $sjson = $kept ? (string) wp_json_encode($kept, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : ''; if ((string) get_post_meta($post_id, self::META_SETTINGS, true) !== $sjson) { if ('' === $sjson) { delete_post_meta($post_id, self::META_SETTINGS); } else { update_post_meta($post_id, self::META_SETTINGS, wp_slash($sjson)); } $changed = true; } if (null !== $notify) { $want = $notify ? 'email' : ''; if ((string) get_post_meta($post_id, self::META_NOTIFY, true) !== $want) { if ('' === $want) { delete_post_meta($post_id, self::META_NOTIFY); } else { update_post_meta($post_id, self::META_NOTIFY, $want); } $changed = true; } } return $changed; } public static function clean_fields(array $fields) { $out = array(); $seen = array(); foreach ($fields as $row) { $f = is_array($row) ? self::normalise_field($row, true) : null; if (!$f || isset($seen[$f['key']])) { continue; } $seen[$f['key']] = true; $stored = array('key' => $f['key'], 'type' => $f['type'], 'label' => $f['label'], 'required' => $f['required']); if ($f['choices']) { $stored['choices'] = $f['choices']; } if ('full' !== $f['width']) { $stored['width'] = $f['width']; } foreach (self::PROPS as $prop) { if ('' !== (string) $f[$prop] && 0 !== $f[$prop]) { $stored[$prop] = $f[$prop]; } } $out[] = $stored; } return $out; } public static function register() { if (self::$registered) { return; } self::$registered = true; add_action('init', array(__CLASS__, 'register_cpt')); add_action('add_meta_boxes', array(__CLASS__, 'add_metabox')); add_action('admin_enqueue_scripts', array(__CLASS__, 'enqueue_editor_assets')); add_action('save_post_' . self::POST_TYPE, array(__CLASS__, 'save_metabox'), 10, 2); add_action('admin_init', array(__CLASS__, 'register_polylang_strings')); add_action('admin_init', array(__CLASS__, 'maybe_install_tables'), 5); } public static function register_cpt() { register_post_type(self::POST_TYPE, array( 'labels' => array( 'name' => __('Forms', 'ai-eshop-optimizer'), 'singular_name' => __('Form', 'ai-eshop-optimizer'), 'add_new_item' => __('Add form', 'ai-eshop-optimizer'), 'edit_item' => __('Edit form', 'ai-eshop-optimizer'), ), 'public' => false, 'show_ui' => true, 'show_in_menu' => false, 'show_in_rest' => false, 'exclude_from_search' => true, 'supports' => array('title'), 'capability_type' => 'post', 'map_meta_cap' => true, )); } public static function maybe_install_tables() { if (get_option(self::DB_VERSION_OPTION) === self::DB_VERSION) { return; } if (!AIEO_Schema_Lock::acquire(self::DB_VERSION_OPTION)) { return; } global $wpdb; $charset = aieo_get_charset_collate(); $e = $wpdb->prefix . 'aieo_form_entries'; $v = $wpdb->prefix . 'aieo_form_entry_values'; $failed = 0; $q = function ($sql) use ($wpdb, &$failed) { if (false === $wpdb->query($sql)) { $failed++; } }; $q("CREATE TABLE IF NOT EXISTS {$e} ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, form_id bigint(20) unsigned NOT NULL, user_id bigint(20) unsigned NOT NULL DEFAULT 0, order_id bigint(20) unsigned NOT NULL DEFAULT 0, source varchar(40) NOT NULL DEFAULT '', surface varchar(64) DEFAULT NULL, external_id bigint(20) unsigned DEFAULT NULL, eponymous_id bigint(20) unsigned NOT NULL DEFAULT 0, run_id bigint(20) unsigned NOT NULL DEFAULT 0, ip_hash char(64) NOT NULL DEFAULT '', status varchar(20) NOT NULL DEFAULT 'new', created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_external (surface, external_id), KEY idx_form_created (form_id, created_at), KEY idx_order (order_id), KEY idx_status (form_id, status), KEY idx_surface (surface, created_at), KEY idx_eponymous (eponymous_id, created_at), KEY idx_run (run_id) ) {$charset}"); self::add_column($q, $e, 'surface', "ALTER TABLE {$e} ADD COLUMN surface varchar(64) DEFAULT NULL AFTER source"); self::add_column($q, $e, 'external_id', "ALTER TABLE {$e} ADD COLUMN external_id bigint(20) unsigned DEFAULT NULL AFTER surface"); self::add_index($q, $e, 'uk_external', "ALTER TABLE {$e} ADD UNIQUE KEY uk_external (surface, external_id)"); self::add_index($q, $e, 'idx_surface', "ALTER TABLE {$e} ADD KEY idx_surface (surface, created_at)"); self::add_column($q, $e, 'eponymous_id', "ALTER TABLE {$e} ADD COLUMN eponymous_id bigint(20) unsigned NOT NULL DEFAULT 0 AFTER external_id"); self::add_column($q, $e, 'run_id', "ALTER TABLE {$e} ADD COLUMN run_id bigint(20) unsigned NOT NULL DEFAULT 0 AFTER eponymous_id"); self::add_index($q, $e, 'idx_eponymous', "ALTER TABLE {$e} ADD KEY idx_eponymous (eponymous_id, created_at)"); self::add_index($q, $e, 'idx_run', "ALTER TABLE {$e} ADD KEY idx_run (run_id)"); $q("CREATE TABLE IF NOT EXISTS {$v} ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, entry_id bigint(20) unsigned NOT NULL, field_key varchar(64) NOT NULL, disposition varchar(12) NOT NULL DEFAULT 'answered', choice_key varchar(64) DEFAULT NULL, value_num decimal(14,4) DEFAULT NULL, value_text longtext, PRIMARY KEY (id), KEY idx_entry (entry_id), KEY idx_field (field_key, choice_key), KEY idx_disposition (field_key, disposition) ) {$charset}"); self::add_column($q, $e, 'page_url', "ALTER TABLE {$e} ADD COLUMN page_url varchar(2048) DEFAULT NULL AFTER source"); self::add_column($q, $e, 'seen_before', "ALTER TABLE {$e} ADD COLUMN seen_before text DEFAULT NULL AFTER page_url"); self::add_column($q, $e, 'location', "ALTER TABLE {$e} ADD COLUMN location varchar(64) DEFAULT NULL AFTER seen_before"); if (0 === $failed) { update_option(self::DB_VERSION_OPTION, self::DB_VERSION, false); } } private static function add_column($q, $table, $column, $sql) { global $wpdb; $has = (int) $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s AND COLUMN_NAME = %s", $table, $column )); if (!$has) { $q($sql); } } private static function add_index($q, $table, $index, $sql) { global $wpdb; $has = (int) $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s AND INDEX_NAME = %s", $table, $index )); if (!$has) { $q($sql); } } public static function entries_table() { global $wpdb; return $wpdb->prefix . 'aieo_form_entries'; } public static function entry_values_table() { global $wpdb; return $wpdb->prefix . 'aieo_form_entry_values'; } public static function fields($form_id) { $raw = get_post_meta((int) $form_id, self::META_FIELDS, true); $arr = is_string($raw) ? json_decode($raw, true) : $raw; if (!is_array($arr)) { return array(); } $out = array(); foreach ($arr as $f) { $field = is_array($f) ? self::normalise_field($f) : null; if ($field) { $out[] = $field; } } return $out; } public static function add_metabox() { add_meta_box( 'aieo-form-fields', __('Fields', 'ai-eshop-optimizer'), array(__CLASS__, 'render_metabox'), self::POST_TYPE, 'normal', 'high' ); add_meta_box( 'aieo-form-settings', __('Words and notification', 'ai-eshop-optimizer'), array(__CLASS__, 'render_settings_box'), self::POST_TYPE, 'normal', 'high' ); add_meta_box( 'aieo-form-button', __('Button', 'ai-eshop-optimizer'), array(__CLASS__, 'render_button_box'), self::POST_TYPE, 'normal', 'high' ); } public static function render_button_box($post) { rooster_admin_builders(); $ui = '\Rooster\Admin\Ui'; $raw = get_post_meta($post->ID, self::META_SETTINGS, true); $stored = is_string($raw) ? json_decode($raw, true) : array(); $stored = is_array($stored) ? $stored : array(); $pages = array('' => __('— Choose a page —', 'ai-eshop-optimizer')); foreach (get_pages(array('sort_column' => 'post_title', 'post_status' => 'publish', 'number' => 300)) as $page) { $pages[(string) $page->ID] = '' !== $page->post_title ? $page->post_title : '#' . $page->ID; } $slug = '' !== (string) $post->post_name ? (string) $post->post_name : (string) $post->ID; $html = '
[aieo_form_button form="' . esc_html($slug) . '"]',
'help' => __('The shortcode, in any theme (a tagDiv theme too); or the block Form Button, which Elementor offers as a widget. A placement may change the words (label="…"), what it opens (opens="modal" or "page"), the buttons beside it (contact="call,whatsapp,viber,messenger", "all" or "none"), their look (icons="hide") and the location (location="its slug").', 'ai-eshop-optimizer'),
));
\Rooster\Admin\Markup::out($html . '' . esc_html('' !== (string) $post->post_name ? (string) $post->post_name : (string) $post->ID) . '' ); ?>
__('Blank: the fields below', 'ai-eshop-optimizer')) + $premade, '', array('id' => 'aieo-form-premade'))); ?>
'key', 'label' => __('Key', 'ai-eshop-optimizer'), 'width' => 'sm'), array('key' => 'type', 'label' => __('Type', 'ai-eshop-optimizer'), 'width' => 'sm'), array('key' => 'label', 'label' => __('Label', 'ai-eshop-optimizer')), array('key' => 'placeholder', 'label' => __('Placeholder', 'ai-eshop-optimizer')), array('key' => 'choices', 'label' => __('Choices (one per line, key|Label)', 'ai-eshop-optimizer')), array('key' => 'width', 'label' => __('Width', 'ai-eshop-optimizer'), 'width' => 'sm'), array('key' => 'more', 'label' => __('More', 'ai-eshop-optimizer')), array('key' => 'required', 'label' => __('Required', 'ai-eshop-optimizer'), 'width' => 'min', 'align' => 'center'), array('key' => 'remove', 'label' => __('Actions', 'ai-eshop-optimizer'), 'type' => 'actions'), ), $table, array( 'id' => 'aieo-form-fields-table', 'caption' => __('The form\'s fields', 'ai-eshop-optimizer'), 'cards' => true, ) )); ?>
'
. ('ack_body' === $k
? $ui::textarea('aieo_form_settings[' . $k . ']', $value, $opts + array('rows' => 4))
: $ui::input('aieo_form_settings[' . $k . ']', $value, $opts))
. '