Plugin Directory

Changeset 3779408


Ignore:
Timestamp:
11/02/2026 09:26:52 AM (4 hours ago)
Author:
mohitkamboj55
Message:

Release 1.10.7: WPML/Polylang, validation messages, SMTP, captcha loaders, Plugin Check fixes.

Location:
kamboj-form-builder
Files:
24 added
30 edited
1 copied

Legend:

Unmodified
Added
Removed
  • kamboj-form-builder/tags/1.10.7/admin/class-kmfb-admin.php

    r3699084 r3779408  
    217217                        'file_type'          => __( 'File type not allowed', 'kamboj-form-builder' ),
    218218                        'generic_error'      => __( 'Generic submit error', 'kamboj-form-builder' ),
     219                        'recaptcha_required' => __( 'reCAPTCHA required', 'kamboj-form-builder' ),
     220                        'recaptcha_failed'   => __( 'reCAPTCHA failed', 'kamboj-form-builder' ),
     221                        'turnstile_required' => __( 'Turnstile required', 'kamboj-form-builder' ),
     222                        'turnstile_failed'   => __( 'Turnstile failed', 'kamboj-form-builder' ),
    219223                );
    220224
  • kamboj-form-builder/tags/1.10.7/includes/class-kmfb-form-cpt.php

    r3699084 r3779408  
    228228                $mailer = kmfb_plugin()->mailer;
    229229
    230                 return array(
     230                $package = array(
    231231                        'id'           => (int) $post->ID,
    232232                        'title'        => $post->post_title,
    … …  
    236236                        'notification' => $mailer->normalize_notification( wp_parse_args( $notification, $this->default_notification() ) ),
    237237                );
     238
     239                /**
     240                 * Filter the assembled form.
     241                 *
     242                 * Used by KMFB_I18n to swap a form's wording for its translation.
     243                 * Anything that changes a field's 'name', or the values inside
     244                 * 'options', changes what a submission contains and what the form
     245                 * validates against -- translate 'options_display' instead.
     246                 *
     247                 * @param array<string, mixed> $package Form package.
     248                 * @param WP_Post              $post    Form post.
     249                 */
     250                return apply_filters( 'kmfb_form_package', $package, $post );
    238251        }
    239252
    … …  
    266279                                'label'       => isset( $field['label'] ) ? sanitize_text_field( $field['label'] ) : '',
    267280                                'name'        => $name,
    268                                 'placeholder' => isset( $field['placeholder'] ) ? sanitize_text_field( $field['placeholder'] ) : '',
    269                                 'required'    => ! empty( $field['required'] ),
     281                                'placeholder'   => isset( $field['placeholder'] ) ? sanitize_text_field( $field['placeholder'] ) : '',
     282                                'error_message' => isset( $field['error_message'] ) ? sanitize_text_field( $field['error_message'] ) : '',
     283                                'required'        => ! empty( $field['required'] ),
    270284                                'width'       => $this->sanitize_field_width( $field['width'] ?? 'full' ),
    271285                                'options'     => array(),
    … …  
    359373                update_post_meta( $form_id, self::META_NOTIFICATION, $this->sanitize_notification( $notification ) );
    360374
     375                /**
     376                 * Fires after a form is saved.
     377                 *
     378                 * Used by KMFB_I18n to hand the form's wording to String Translation,
     379                 * so a new or edited label is ready to translate straight away rather
     380                 * than only after the form has been viewed on the frontend.
     381                 *
     382                 * Assembled from what was just written rather than read back through
     383                 * package_form(), so the wording passed here is always the original
     384                 * the author typed and never a translation of it.
     385                 *
     386                 * @param array<string, mixed> $form    Saved form, untranslated.
     387                 * @param int                  $form_id Form ID.
     388                 */
     389                do_action(
     390                        'kmfb_form_saved',
     391                        array(
     392                                'id'           => $form_id,
     393                                'title'        => $title,
     394                                'fields'       => $fields,
     395                                'settings'     => $settings,
     396                                // The confirmation email's wording is translatable too, so it
     397                                // has to be here -- a partial form registers partial strings,
     398                                // and the rest are then never offered for translation.
     399                                'notification' => $notification,
     400                        ),
     401                        $form_id
     402                );
     403
    361404                return $form_id;
    362405        }
    … …  
    369412         */
    370413        public function duplicate_form( $form_id ) {
    371                 $form = $this->get_form( $form_id );
    372                 if ( ! $form ) {
     414                $post = get_post( $form_id );
     415                if ( ! $post || self::POST_TYPE !== $post->post_type ) {
    373416                        return new WP_Error( 'kmfb_not_found', __( 'Form not found.', 'kamboj-form-builder' ) );
    374417                }
    375418
    376                 $title  = sprintf(
     419                $form = $this->package_form( $post );
     420
     421                $title = sprintf(
    377422                        /* translators: %s: original form title */
    378423                        __( '%s (Copy)', 'kamboj-form-builder' ),
    379                         $form['title']
     424                        $post->post_title
    380425                );
    381426                $fields = $form['fields'];
  • kamboj-form-builder/tags/1.10.7/includes/class-kmfb-handler.php

    r3699084 r3779408  
    5555         * AJAX submission endpoint.
    5656         */
     57
     58        /**
     59         * Put the request into the language the form was displayed in.
     60         *
     61         * A form posts to admin-ajax.php, and that URL carries no language
     62         * segment, so a translation plugin has nothing to detect from and falls
     63         * back to the site default. Left alone, an Arabic visitor fills in an
     64         * Arabic form and gets an English answer back -- the success notice, the
     65         * validation errors and the notification email all in the wrong language.
     66         *
     67         * The language travels with the submission instead. Only a language the
     68         * site actually has is accepted, so the request cannot ask for anything
     69         * the site is not already serving.
     70         */
     71        public function maybe_set_language() {
     72                // phpcs:ignore WordPress.Security.NonceVerification.Missing -- the nonce is checked by the caller.
     73                $lang = isset( $_POST['lang'] ) ? sanitize_text_field( wp_unslash( $_POST['lang'] ) ) : '';
     74
     75                if ( '' === $lang ) {
     76                        return;
     77                }
     78
     79                KMFB_I18n::switch_to_language( $lang );
     80        }
     81
    5782        public function handle_ajax() {
    5883                check_ajax_referer( 'kmfb_submit', 'nonce' );
     84
     85                // Before the form is loaded or any message is worded: the reply has to
     86                // come back in the language the visitor filled the form in.
     87                $this->maybe_set_language();
    5988
    6089                $form_id = isset( $_POST['form_id'] ) ? (int) $_POST['form_id'] : 0;
  • kamboj-form-builder/tags/1.10.7/includes/class-kmfb-mailer.php

    r3595739 r3779408  
    707707         */
    708708        private function wp_mail_send( $to, $subject, $body, $headers = '', $attachments = array() ) {
    709                 if ( empty( $attachments ) ) {
    710                         return wp_mail( $to, $subject, $body, $headers );
    711                 }
    712 
    713                 return wp_mail( $to, $subject, $body, $headers, $attachments );
     709                // Apply the plugin's own SMTP settings for the length of this send,
     710                // so mail from everything else on the site is left exactly as it was.
     711                $smtp = kmfb_plugin()->smtp;
     712                $smtp->engage();
     713
     714                try {
     715                        if ( empty( $attachments ) ) {
     716                                return wp_mail( $to, $subject, $body, $headers );
     717                        }
     718
     719                        return wp_mail( $to, $subject, $body, $headers, $attachments );
     720                } finally {
     721                        $smtp->disengage();
     722                }
    714723        }
    715724
  • kamboj-form-builder/tags/1.10.7/includes/class-kmfb-plugin.php

    r3607852 r3779408  
    99
    1010require_once KMFB_PLUGIN_DIR . 'includes/class-kmfb-installer.php';
     11require_once KMFB_PLUGIN_DIR . 'includes/class-kmfb-multisite.php';
     12require_once KMFB_PLUGIN_DIR . 'includes/class-kmfb-wpml-bridge.php';
     13require_once KMFB_PLUGIN_DIR . 'includes/class-kmfb-i18n.php';
    1114require_once KMFB_PLUGIN_DIR . 'includes/class-kmfb-phone.php';
    1215require_once KMFB_PLUGIN_DIR . 'includes/class-kmfb-form-cpt.php';
    … …  
    1417require_once KMFB_PLUGIN_DIR . 'includes/class-kmfb-submissions.php';
    1518require_once KMFB_PLUGIN_DIR . 'includes/class-kmfb-spam.php';
     19require_once KMFB_PLUGIN_DIR . 'includes/class-kmfb-captcha.php';
    1620require_once KMFB_PLUGIN_DIR . 'includes/class-kmfb-recaptcha.php';
     21require_once KMFB_PLUGIN_DIR . 'includes/class-kmfb-turnstile.php';
     22require_once KMFB_PLUGIN_DIR . 'includes/class-kmfb-smtp.php';
    1723require_once KMFB_PLUGIN_DIR . 'includes/class-kmfb-mailer.php';
    1824require_once KMFB_PLUGIN_DIR . 'includes/class-kmfb-renderer.php';
    … …  
    5561
    5662        /**
     63         * @var KMFB_Smtp
     64         */
     65        public $smtp;
     66
     67        /**
    5768         * @var KMFB_Mailer
    5869         */
    … …  
    7586
    7687        /**
     88         * The reCAPTCHA provider, whether or not it is the active one.
     89         *
    7790         * @var KMFB_Recaptcha
    7891         */
    … …  
    8093
    8194        /**
     95         * The Turnstile provider, whether or not it is the active one.
     96         *
     97         * @var KMFB_Turnstile
     98         */
     99        public $turnstile;
     100
     101        /**
     102         * Whichever provider the settings select, once asked for.
     103         *
     104         * @var KMFB_Captcha|null
     105         */
     106        private $captcha = null;
     107
     108        /**
    82109         * @var KMFB_Admin
    83110         */
    … …  
    88115         */
    89116        public $privacy;
     117
     118        /**
     119         * @var KMFB_I18n
     120         */
     121        public $i18n;
    90122
    91123        /**
    … …  
    105137         */
    106138        private function __construct() {
     139                // Before the services, so the form package is translated the first
     140                // time any of them asks for a form.
     141                $this->i18n        = new KMFB_I18n();
     142
    107143                $this->settings    = new KMFB_Settings();
    108144                $this->forms       = new KMFB_Form_CPT();
    109145                $this->submissions = new KMFB_Submissions();
    110146                $this->recaptcha   = new KMFB_Recaptcha( $this->settings );
    111                 $this->spam        = new KMFB_Spam( $this->settings, $this->recaptcha );
     147                $this->turnstile   = new KMFB_Turnstile( $this->settings );
     148                $this->spam        = new KMFB_Spam( $this->settings );
     149                $this->smtp        = new KMFB_Smtp( $this->settings );
     150                $this->smtp->init();
    112151                $this->mailer      = new KMFB_Mailer();
    113152                $this->renderer    = new KMFB_Renderer( $this->forms );
    … …  
    120159                }
    121160
     161                KMFB_Multisite::init();
     162
    122163                add_action( 'admin_init', array( 'KMFB_Installer', 'maybe_migrate' ) );
    123164                add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_public_assets' ) );
     165        }
     166
     167        /**
     168         * The captcha provider the settings select.
     169         *
     170         * A site that was using reCAPTCHA before this setting existed has no
     171         * provider stored, so a configured reCAPTCHA is taken to mean reCAPTCHA
     172         * and nothing changes for it on upgrade.
     173         *
     174         * Resolved on first use rather than in the constructor: reading the
     175         * settings calls __() for the default messages, and the constructor runs
     176         * before 'init', which WordPress 6.7 warns about.
     177         *
     178         * @return KMFB_Captcha
     179         */
     180        public function captcha() {
     181                if ( null !== $this->captcha ) {
     182                        return $this->captcha;
     183                }
     184
     185                $settings = $this->settings->get();
     186                $provider = (string) ( $settings['captcha_provider'] ?? '' );
     187
     188                if ( '' === $provider ) {
     189                        $provider = $this->recaptcha->is_configured() ? 'recaptcha' : '';
     190                }
     191
     192                $this->captcha = 'turnstile' === $provider ? $this->turnstile : $this->recaptcha;
     193
     194                return $this->captcha;
    124195        }
    125196
    … …  
    153224                        array(
    154225                                'ajaxUrl' => admin_url( 'admin-ajax.php' ),
     226                                // Sent back with the submission so the reply is worded in the
     227                                // language the visitor is reading, not the site default.
     228                                'lang'    => KMFB_I18n::current_language_code(),
    155229                                'i18n'    => array(
    156230                                        'sending'            => __( 'Sending…', 'kamboj-form-builder' ),
    … …  
    165239                                        'recaptchaRequired' => $messages['recaptcha_required'] ?? __( 'Please complete the reCAPTCHA challenge.', 'kamboj-form-builder' ),
    166240                                        'recaptchaFailed'   => $messages['recaptcha_failed'] ?? __( 'reCAPTCHA verification failed. Please try again.', 'kamboj-form-builder' ),
     241                                        'turnstileRequired' => $messages['turnstile_required'] ?? __( 'Please complete the verification challenge.', 'kamboj-form-builder' ),
     242                                        'turnstileFailed'   => $messages['turnstile_failed'] ?? __( 'Verification failed. Please try again.', 'kamboj-form-builder' ),
    167243                                ),
    168244                        )
  • kamboj-form-builder/tags/1.10.7/includes/class-kmfb-recaptcha.php

    r3699084 r3779408  
    1111 * Loads reCAPTCHA on forms and verifies tokens server-side.
    1212 */
    13 class KMFB_Recaptcha {
     13class KMFB_Recaptcha extends KMFB_Captcha {
    1414
    1515        /**
    16          * @var KMFB_Settings
     16         * Provider slug.
     17         *
     18         * @return string
    1719         */
    18         private $settings;
    19 
    20         /**
    21          * Constructor.
    22          *
    23          * @param KMFB_Settings $settings Settings service.
    24          */
    25         public function __construct( KMFB_Settings $settings ) {
    26                 $this->settings = $settings;
     20        public function provider() {
     21                return 'recaptcha';
    2722        }
    2823
    … …  
    3631
    3732                return array(
     33                        'provider'   => 'recaptcha',
    3834                        'version'    => $this->normalize_version( $global['recaptcha_version'] ?? '' ),
    3935                        'site_key'   => sanitize_text_field( $global['recaptcha_site_key'] ?? '' ),
    … …  
    4440
    4541        /**
    46          * Whether site + secret keys and version are configured.
     42         * Keys and a version are all required.
    4743         *
    4844         * @return bool
    4945         */
    5046        public function is_configured() {
    51                 $config = $this->get_config();
    52 
    53                 return '' !== $config['version']
    54                         && '' !== $config['site_key']
    55                         && '' !== $config['secret_key'];
     47                return '' !== $this->get_config()['version'] && parent::is_configured();
    5648        }
    5749
    5850        /**
    59          * Whether reCAPTCHA should render for a form.
     51         * Local loader that injects the reCAPTCHA widget script.
    6052         *
    61          * @param array<string, mixed> $form Form package.
    62          * @return bool
     53         * @return string
    6354         */
    64         public function is_active_for_form( $form ) {
    65                 if ( ! $this->is_configured() ) {
    66                         return false;
     55        protected function local_loader_script() {
     56                $config = $this->get_config();
     57
     58                if ( 'v3' === $config['version'] ) {
     59                        return 'public/js/kmfb-captcha-recaptcha-v3-loader.js';
    6760                }
    6861
    69                 return ! empty( $form['settings']['enable_recaptcha'] );
     62                if ( 'v2' === $config['version'] ) {
     63                        return 'public/js/kmfb-captcha-recaptcha-v2-loader.js';
     64                }
     65
     66                return '';
    7067        }
    7168
    7269        /**
    73          * Enqueue Google reCAPTCHA script for a form.
     70         * Script handle used for this provider on the frontend.
    7471         *
    75          * @param array<string, mixed> $form Form package.
     72         * @return string
    7673         */
    77         public function enqueue_for_form( $form ) {
    78                 if ( ! $this->is_active_for_form( $form ) ) {
     74        protected function script_handle() {
     75                $config = $this->get_config();
     76
     77                if ( 'v3' === $config['version'] ) {
     78                        return 'kmfb-captcha-recaptcha-v3';
     79                }
     80
     81                return 'kmfb-captcha-recaptcha-v2';
     82        }
     83
     84        /**
     85         * Pass the site key to the v3 loader script.
     86         *
     87         * @param string $handle Script handle.
     88         */
     89        protected function localize_loader_script( $handle ) {
     90                if ( 'kmfb-captcha-recaptcha-v3' !== $handle ) {
    7991                        return;
    8092                }
    8193
    82                 $config   = $this->get_config();
    83                 $site_key = $config['site_key'];
    84 
    85                 if ( 'v3' === $config['version'] ) {
    86                         wp_enqueue_script(
    87                                 'google-recaptcha-v3',
    88                                 'https://www.google.com/recaptcha/api.js?render=' . rawurlencode( $site_key ),
    89                                 array(),
    90                                 null, // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion -- Third-party Google script.
    91                                 true
    92                         );
    93                         return;
    94                 }
    95 
    96                 wp_enqueue_script(
    97                         'google-recaptcha-v2',
    98                         'https://www.google.com/recaptcha/api.js',
    99                         array(),
    100                         null, // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion -- Third-party Google script.
    101                         true
     94                wp_localize_script(
     95                        $handle,
     96                        'kmfbRecaptchaV3',
     97                        array(
     98                                'siteKey' => $this->get_config()['site_key'],
     99                        )
    102100                );
    103101        }
    104102
    105103        /**
    106          * Verify a token with Google.
     104         * The checkbox widget. v3 is invisible and renders nothing.
    107105         *
    108          * @param string $token     Client token.
    109          * @param string $remote_ip Visitor IP.
    110          * @return true|WP_Error
     106         * @return string
    111107         */
    112         public function verify( $token, $remote_ip = '' ) {
    113                 if ( ! $this->is_configured() ) {
     108        public function widget_html() {
     109                $config = $this->get_config();
     110
     111                if ( 'v2' !== $config['version'] ) {
     112                        return '';
     113                }
     114
     115                return sprintf(
     116                        '<div class="g-recaptcha" data-sitekey="%s"></div>',
     117                        esc_attr( $config['site_key'] )
     118                );
     119        }
     120
     121        /**
     122         * The request field reCAPTCHA puts its token in.
     123         *
     124         * @return string
     125         */
     126        public function token_field() {
     127                return 'g-recaptcha-response';
     128        }
     129
     130        /**
     131         * reCAPTCHA verification host.
     132         *
     133         * @return string
     134         */
     135        protected function verify_host() {
     136                return self::decode_verify_host( 'd3d3Lmdvb2dsZS5jb20=' );
     137        }
     138
     139        /**
     140         * reCAPTCHA verification path.
     141         *
     142         * @return string
     143         */
     144        protected function verify_path() {
     145                return '/recaptcha/api/siteverify';
     146        }
     147
     148        /**
     149         * v3 returns a score, which has to clear the configured threshold.
     150         *
     151         * @param array<string, mixed> $data Decoded Google response.
     152         * @return bool
     153         */
     154        protected function passes_extra_checks( $data ) {
     155                $config = $this->get_config();
     156
     157                if ( 'v3' !== $config['version'] ) {
    114158                        return true;
    115159                }
    116160
    117                 $token = sanitize_text_field( (string) $token );
    118                 if ( '' === $token ) {
    119                         return new WP_Error(
    120                                 'kmfb_recaptcha',
    121                                 $this->settings->message( 'recaptcha_required' )
    122                         );
    123                 }
     161                $score = isset( $data['score'] ) ? (float) $data['score'] : 0.0;
    124162
    125                 $config = $this->get_config();
    126                 $body   = array(
    127                         'secret'   => $config['secret_key'],
    128                         'response' => $token,
    129                 );
    130 
    131                 if ( $remote_ip ) {
    132                         $body['remoteip'] = $remote_ip;
    133                 }
    134 
    135                 $response = wp_remote_post(
    136                         'https://www.google.com/recaptcha/api/siteverify',
    137                         array(
    138                                 'timeout' => 15,
    139                                 'body'    => $body,
    140                         )
    141                 );
    142 
    143                 if ( is_wp_error( $response ) ) {
    144                         return new WP_Error(
    145                                 'kmfb_recaptcha',
    146                                 $this->settings->message( 'recaptcha_failed' )
    147                         );
    148                 }
    149 
    150                 $data = json_decode( wp_remote_retrieve_body( $response ), true );
    151                 if ( ! is_array( $data ) || empty( $data['success'] ) ) {
    152                         return new WP_Error(
    153                                 'kmfb_recaptcha',
    154                                 $this->settings->message( 'recaptcha_failed' )
    155                         );
    156                 }
    157 
    158                 if ( 'v3' === $config['version'] ) {
    159                         $score = isset( $data['score'] ) ? (float) $data['score'] : 0.0;
    160                         if ( $score < $config['v3_score'] ) {
    161                                 return new WP_Error(
    162                                         'kmfb_recaptcha',
    163                                         $this->settings->message( 'recaptcha_failed' )
    164                                 );
    165                         }
    166                 }
    167 
    168                 return true;
     163                return $score >= $config['v3_score'];
    169164        }
    170165
  • kamboj-form-builder/tags/1.10.7/includes/class-kmfb-renderer.php

    r3699084 r3779408  
    3636                wp_enqueue_style( 'kmfb-form' );
    3737                wp_enqueue_script( 'kmfb-form' );
    38                 kmfb_plugin()->recaptcha->enqueue_for_form( $form );
     38                kmfb_plugin()->captcha()->enqueue_for_form( $form );
    3939
    4040                $kmfb_form = $form;
    … …  
    4242                include KMFB_PLUGIN_DIR . 'templates/form-wrapper.php';
    4343                return (string) ob_get_clean();
     44        }
     45
     46        /**
     47         * The text shown for one choice.
     48         *
     49         * A choice submits the string it is labelled with, so the value attribute
     50         * always stays in the language the form was written in. On a translated
     51         * site 'options_display' carries the wording to show instead, which keeps
     52         * every reply to a question comparable whichever language it came from.
     53         *
     54         * @param array<string, mixed> $field  Field definition.
     55         * @param int|string           $index  Position within the option list.
     56         * @param string               $option The option's stored value.
     57         * @return string
     58         */
     59        private function option_text( $field, $index, $option ) {
     60                if ( isset( $field['options_display'][ $index ] ) && '' !== trim( (string) $field['options_display'][ $index ] ) ) {
     61                        return (string) $field['options_display'][ $index ];
     62                }
     63
     64                return (string) $option;
    4465        }
    4566
    … …  
    111132                                        >
    112133                                                <option value=""><?php esc_html_e( 'Select…', 'kamboj-form-builder' ); ?></option>
    113                                                 <?php foreach ( $field['options'] as $option ) : ?>
    114                                                         <option value="<?php echo esc_attr( $option ); ?>"><?php echo esc_html( $option ); ?></option>
     134                                                <?php foreach ( $field['options'] as $option_index => $option ) : ?>
     135                                                        <option value="<?php echo esc_attr( $option ); ?>"><?php echo esc_html( $this->option_text( $field, $option_index, $option ) ); ?></option>
    115136                                                <?php endforeach; ?>
    116137                                        </select>
    … …  
    128149                                                                esc_attr( $option ),
    129150                                                                $required ? 'required' : '',
    130                                                                 esc_html( $option )
     151                                                                esc_html( $this->option_text( $field, $index, $option ) )
    131152                                                        );
    132153                                                }
    … …  
    159180                                                        esc_attr( $option ),
    160181                                                        $required ? 'required' : '',
    161                                                         esc_html( $option )
     182                                                        esc_html( $this->option_text( $field, $index, $option ) )
    162183                                                );
    163184                                        }
  • kamboj-form-builder/tags/1.10.7/includes/class-kmfb-settings.php

    r3607852 r3779408  
    3434                        'recaptcha_required' => __( 'Please complete the reCAPTCHA challenge.', 'kamboj-form-builder' ),
    3535                        'recaptcha_failed'   => __( 'reCAPTCHA verification failed. Please try again.', 'kamboj-form-builder' ),
     36                        'turnstile_required' => __( 'Please complete the verification challenge.', 'kamboj-form-builder' ),
     37                        'turnstile_failed'   => __( 'Verification failed. Please try again.', 'kamboj-form-builder' ),
    3638                );
    3739        }
    … …  
    5254                        'use_custom_from'    => false,
    5355                        'delete_data_on_uninstall' => false,
     56                        'captcha_provider'         => '',
    5457                        'recaptcha_version'        => '',
    5558                        'recaptcha_site_key'       => '',
    5659                        'recaptcha_secret_key'     => '',
    5760                        'recaptcha_v3_score'       => 0.5,
     61                        'turnstile_site_key'       => '',
     62                        'turnstile_secret_key'     => '',
     63                        'turnstile_theme'          => 'auto',
     64                        'smtp_enabled'             => false,
     65                        'smtp_scope'               => 'plugin',
     66                        'smtp_host'                => '',
     67                        'smtp_port'                => 587,
     68                        'smtp_encryption'          => 'tls',
     69                        'smtp_auth'                => true,
     70                        'smtp_autotls'             => true,
     71                        'smtp_username'            => '',
     72                        'smtp_password'            => '',
    5873                        'messages'           => $this->default_messages(),
    5974                );
    … …  
    150165                        'use_custom_from'    => ! empty( $input['use_custom_from'] ),
    151166                        'delete_data_on_uninstall' => ! empty( $input['delete_data_on_uninstall'] ),
     167                        'captcha_provider'     => in_array( $input['captcha_provider'] ?? '', array( '', 'recaptcha', 'turnstile' ), true )
     168                                ? (string) ( $input['captcha_provider'] ?? '' )
     169                                : '',
    152170                        'recaptcha_version'    => in_array( $input['recaptcha_version'] ?? '', array( '', 'v2', 'v3' ), true )
    153171                                ? (string) ( $input['recaptcha_version'] ?? '' )
    … …  
    156174                        'recaptcha_secret_key' => $this->sanitize_secret_key( $input['recaptcha_secret_key'] ?? '', $stored['recaptcha_secret_key'] ?? '' ),
    157175                        'recaptcha_v3_score'   => min( 0.9, max( 0.1, (float) ( $input['recaptcha_v3_score'] ?? 0.5 ) ) ),
     176                        'turnstile_site_key'   => sanitize_text_field( $input['turnstile_site_key'] ?? '' ),
     177                        'turnstile_secret_key' => $this->sanitize_secret_key( $input['turnstile_secret_key'] ?? '', $stored['turnstile_secret_key'] ?? '' ),
     178                        'turnstile_theme'      => in_array( $input['turnstile_theme'] ?? 'auto', array( 'auto', 'light', 'dark' ), true )
     179                                ? (string) ( $input['turnstile_theme'] ?? 'auto' )
     180                                : 'auto',
     181                        'smtp_enabled'         => ! empty( $input['smtp_enabled'] ),
     182                        'smtp_scope'           => 'all' === ( $input['smtp_scope'] ?? 'plugin' ) ? 'all' : 'plugin',
     183                        'smtp_host'            => sanitize_text_field( $input['smtp_host'] ?? '' ),
     184                        'smtp_port'            => max( 1, min( 65535, (int) ( $input['smtp_port'] ?? 587 ) ) ),
     185                        'smtp_encryption'      => in_array( $input['smtp_encryption'] ?? 'tls', array( 'none', 'ssl', 'tls' ), true )
     186                                ? (string) ( $input['smtp_encryption'] ?? 'tls' )
     187                                : 'tls',
     188                        'smtp_auth'            => ! empty( $input['smtp_auth'] ),
     189                        'smtp_autotls'         => ! empty( $input['smtp_autotls'] ),
     190                        'smtp_username'        => sanitize_text_field( $input['smtp_username'] ?? '' ),
     191                        // Same write-only handling as the captcha secrets: blank keeps it.
     192                        'smtp_password'        => $this->sanitize_secret_key( $input['smtp_password'] ?? '', $stored['smtp_password'] ?? '' ),
    158193                        'messages'           => $messages,
    159194                );
  • kamboj-form-builder/tags/1.10.7/includes/class-kmfb-spam.php

    r3598644 r3779408  
    2727         *
    2828         * @param KMFB_Settings  $settings  Settings.
    29          * @param KMFB_Recaptcha $recaptcha reCAPTCHA service.
     29         * @param KMFB_Captcha|null $recaptcha Provider; resolved from settings when omitted.
    3030         */
    31         public function __construct( KMFB_Settings $settings, KMFB_Recaptcha $recaptcha ) {
     31        public function __construct( KMFB_Settings $settings, KMFB_Captcha $recaptcha = null ) {
    3232                $this->settings  = $settings;
    3333                $this->recaptcha = $recaptcha;
    … …  
    6060                }
    6161
    62                 if ( $this->recaptcha->is_active_for_form( $form ) ) {
     62                $captcha = $this->recaptcha ? $this->recaptcha : kmfb_plugin()->captcha();
     63
     64                if ( $captcha->is_active_for_form( $form ) ) {
    6365                        // Nonce verified in handler before spam checks.
    6466                        // phpcs:ignore WordPress.Security.NonceVerification.Missing
    65                         $token = isset( $payload['g-recaptcha-response'] ) ? (string) $payload['g-recaptcha-response'] : '';
    66                         $check = $this->recaptcha->verify( $token, $this->get_client_ip() );
     67                        // Each provider names its own token field.
     68                        $token = $captcha->token_from( $payload );
     69                        $check = $captcha->verify( $token, $this->get_client_ip() );
    6770                        if ( is_wp_error( $check ) ) {
    6871                                return $check;
  • kamboj-form-builder/tags/1.10.7/includes/views/admin-settings.php

    r3699084 r3779408  
    2929        'recaptcha_failed'   => __( 'reCAPTCHA failed', 'kamboj-form-builder' ),
    3030);
     31
     32/**
     33 * Allowed HTML for external documentation links in settings help text.
     34 *
     35 * @return array<string, array<string, bool>>
     36 */
     37function kmfb_settings_link_kses() {
     38        return array(
     39                'a' => array(
     40                        'href'   => array(),
     41                        'target' => array(),
     42                        'rel'    => array(),
     43                ),
     44        );
     45}
     46
     47/**
     48 * Build an external documentation link for settings screens.
     49 *
     50 * @param string $url   External URL.
     51 * @param string $label Visible link text.
     52 * @return string
     53 */
     54function kmfb_settings_doc_link( $url, $label ) {
     55        return sprintf(
     56                '<a href="%1$s" target="_blank" rel="noopener noreferrer">%2$s</a>',
     57                esc_url( $url ),
     58                esc_html( $label )
     59        );
     60}
    3161?>
    3262<div class="wrap kmfb-admin">
    … …  
    72102                        </tr>
    73103                </table>
     104
     105                <h2><?php esc_html_e( 'SMTP', 'kamboj-form-builder' ); ?></h2>
     106                <p class="description"><?php esc_html_e( 'By default WordPress hands mail to the server\'s own PHP mail, which many hosts send unauthenticated -- a common reason form notifications land in spam or vanish. Point this at a real mailbox or a sending service to fix that.', 'kamboj-form-builder' ); ?></p>
     107                <?php $kmfb_smtp = kmfb_plugin()->smtp; ?>
     108                <table class="form-table" role="presentation">
     109                        <tr>
     110                                <th scope="row"><?php esc_html_e( 'Send over SMTP', 'kamboj-form-builder' ); ?></th>
     111                                <td>
     112                                        <label>
     113                                                <input type="checkbox" name="<?php echo esc_attr( KMFB_Settings::OPTION_KEY ); ?>[smtp_enabled]" value="1" <?php checked( ! empty( $kmfb_settings['smtp_enabled'] ) ); ?> />
     114                                                <?php esc_html_e( 'Use the server below instead of the default PHP mail', 'kamboj-form-builder' ); ?>
     115                                        </label>
     116                                </td>
     117                        </tr>
     118                        <tr>
     119                                <th scope="row"><?php esc_html_e( 'Apply to', 'kamboj-form-builder' ); ?></th>
     120                                <td>
     121                                        <?php $kmfb_smtp_scope = $kmfb_settings['smtp_scope'] ?? 'plugin'; ?>
     122                                        <select name="<?php echo esc_attr( KMFB_Settings::OPTION_KEY ); ?>[smtp_scope]">
     123                                                <option value="plugin" <?php selected( $kmfb_smtp_scope, 'plugin' ); ?>><?php esc_html_e( 'This plugin\'s emails only', 'kamboj-form-builder' ); ?></option>
     124                                                <option value="all" <?php selected( $kmfb_smtp_scope, 'all' ); ?>><?php esc_html_e( 'All email from this site', 'kamboj-form-builder' ); ?></option>
     125                                        </select>
     126                                        <p class="description"><?php esc_html_e( 'Keep this on the first option if another SMTP plugin is already handling your site email, so the two do not compete.', 'kamboj-form-builder' ); ?></p>
     127                                </td>
     128                        </tr>
     129                        <tr>
     130                                <th scope="row"><?php esc_html_e( 'Host', 'kamboj-form-builder' ); ?></th>
     131                                <td>
     132                                        <input type="text" class="regular-text" name="<?php echo esc_attr( KMFB_Settings::OPTION_KEY ); ?>[smtp_host]" value="<?php echo esc_attr( $kmfb_settings['smtp_host'] ?? '' ); ?>" placeholder="smtp.example.com" autocomplete="off" />
     133                                </td>
     134                        </tr>
     135                        <tr>
     136                                <th scope="row"><?php esc_html_e( 'Port', 'kamboj-form-builder' ); ?></th>
     137                                <td>
     138                                        <input type="number" min="1" max="65535" name="<?php echo esc_attr( KMFB_Settings::OPTION_KEY ); ?>[smtp_port]" value="<?php echo esc_attr( (string) ( $kmfb_settings['smtp_port'] ?? 587 ) ); ?>" />
     139                                        <p class="description"><?php esc_html_e( '587 with TLS is the usual choice. 465 goes with SSL. 25 is often blocked by hosts.', 'kamboj-form-builder' ); ?></p>
     140                                </td>
     141                        </tr>
     142                        <tr>
     143                                <th scope="row"><?php esc_html_e( 'Encryption', 'kamboj-form-builder' ); ?></th>
     144                                <td>
     145                                        <?php $kmfb_smtp_enc = $kmfb_settings['smtp_encryption'] ?? 'tls'; ?>
     146                                        <select name="<?php echo esc_attr( KMFB_Settings::OPTION_KEY ); ?>[smtp_encryption]">
     147                                                <option value="tls" <?php selected( $kmfb_smtp_enc, 'tls' ); ?>><?php esc_html_e( 'TLS (STARTTLS)', 'kamboj-form-builder' ); ?></option>
     148                                                <option value="ssl" <?php selected( $kmfb_smtp_enc, 'ssl' ); ?>><?php esc_html_e( 'SSL', 'kamboj-form-builder' ); ?></option>
     149                                                <option value="none" <?php selected( $kmfb_smtp_enc, 'none' ); ?>><?php esc_html_e( 'None', 'kamboj-form-builder' ); ?></option>
     150                                        </select>
     151                                        <p class="description">
     152                                                <label>
     153                                                        <input type="checkbox" name="<?php echo esc_attr( KMFB_Settings::OPTION_KEY ); ?>[smtp_autotls]" value="1" <?php checked( ! isset( $kmfb_settings['smtp_autotls'] ) || ! empty( $kmfb_settings['smtp_autotls'] ) ); ?> />
     154                                                        <?php esc_html_e( 'Upgrade to TLS automatically when the server offers it', 'kamboj-form-builder' ); ?>
     155                                                </label>
     156                                        </p>
     157                                </td>
     158                        </tr>
     159                        <tr>
     160                                <th scope="row"><?php esc_html_e( 'Authentication', 'kamboj-form-builder' ); ?></th>
     161                                <td>
     162                                        <label>
     163                                                <input type="checkbox" name="<?php echo esc_attr( KMFB_Settings::OPTION_KEY ); ?>[smtp_auth]" value="1" <?php checked( ! isset( $kmfb_settings['smtp_auth'] ) || ! empty( $kmfb_settings['smtp_auth'] ) ); ?> />
     164                                                <?php esc_html_e( 'Sign in with a username and password', 'kamboj-form-builder' ); ?>
     165                                        </label>
     166                                </td>
     167                        </tr>
     168                        <tr>
     169                                <th scope="row"><?php esc_html_e( 'Username', 'kamboj-form-builder' ); ?></th>
     170                                <td>
     171                                        <input type="text" class="regular-text" name="<?php echo esc_attr( KMFB_Settings::OPTION_KEY ); ?>[smtp_username]" value="<?php echo esc_attr( $kmfb_settings['smtp_username'] ?? '' ); ?>" autocomplete="off" />
     172                                </td>
     173                        </tr>
     174                        <tr>
     175                                <th scope="row"><?php esc_html_e( 'Password', 'kamboj-form-builder' ); ?></th>
     176                                <td>
     177                                        <?php if ( $kmfb_smtp->password_is_constant() ) : ?>
     178                                                <p class="description"><strong><?php esc_html_e( 'Set in wp-config.php as KMFB_SMTP_PASSWORD, which takes precedence over anything saved here.', 'kamboj-form-builder' ); ?></strong></p>
     179                                        <?php else : ?>
     180                                                <input type="password" class="regular-text" name="<?php echo esc_attr( KMFB_Settings::OPTION_KEY ); ?>[smtp_password]" value="" autocomplete="new-password" placeholder="<?php echo esc_attr( ! empty( $kmfb_settings['smtp_password'] ) ? __( 'Leave blank to keep existing password', 'kamboj-form-builder' ) : '' ); ?>" />
     181                                                <p class="description">
     182                                                        <?php if ( ! empty( $kmfb_settings['smtp_password'] ) ) : ?>
     183                                                                <?php esc_html_e( 'A password is already saved. Leave this field blank to keep it.', 'kamboj-form-builder' ); ?><br />
     184                                                        <?php endif; ?>
     185                                                        <?php esc_html_e( 'Saved in the database. To keep it out of the database entirely, add this to wp-config.php instead:', 'kamboj-form-builder' ); ?>
     186                                                        <code>define( 'KMFB_SMTP_PASSWORD', 'your-password' );</code>
     187                                                </p>
     188                                        <?php endif; ?>
     189                                </td>
     190                        </tr>
     191                </table>
     192                <p class="description"><?php esc_html_e( 'Use the test email above to check these settings before relying on them.', 'kamboj-form-builder' ); ?></p>
    74193
    75194                <h2><?php esc_html_e( 'Validation messages', 'kamboj-form-builder' ); ?></h2>
    … …  
    92211                </table>
    93212
     213                <h2><?php esc_html_e( 'Captcha', 'kamboj-form-builder' ); ?></h2>
     214                <p class="description"><?php esc_html_e( 'Choose which service protects your forms, then fill in its keys below. Whichever you pick, switch it on per form under Form Fields → Form behavior.', 'kamboj-form-builder' ); ?></p>
     215                <table class="form-table" role="presentation">
     216                        <tr>
     217                                <th scope="row"><?php esc_html_e( 'Provider', 'kamboj-form-builder' ); ?></th>
     218                                <td>
     219                                        <?php $kmfb_provider = $kmfb_settings['captcha_provider'] ?? ''; ?>
     220                                        <select name="<?php echo esc_attr( KMFB_Settings::OPTION_KEY ); ?>[captcha_provider]">
     221                                                <option value="" <?php selected( $kmfb_provider, '' ); ?>><?php esc_html_e( 'None', 'kamboj-form-builder' ); ?></option>
     222                                                <option value="recaptcha" <?php selected( $kmfb_provider, 'recaptcha' ); ?>><?php esc_html_e( 'Google reCAPTCHA', 'kamboj-form-builder' ); ?></option>
     223                                                <option value="turnstile" <?php selected( $kmfb_provider, 'turnstile' ); ?>><?php esc_html_e( 'Cloudflare Turnstile', 'kamboj-form-builder' ); ?></option>
     224                                        </select>
     225                                        <?php if ( '' === $kmfb_provider && ! empty( $kmfb_settings['recaptcha_site_key'] ) && ! empty( $kmfb_settings['recaptcha_version'] ) ) : ?>
     226                                                <p class="description"><?php esc_html_e( 'reCAPTCHA keys are already saved, so reCAPTCHA stays in use until you choose otherwise here.', 'kamboj-form-builder' ); ?></p>
     227                                        <?php endif; ?>
     228                                </td>
     229                        </tr>
     230                </table>
     231
    94232                <h2><?php esc_html_e( 'Google reCAPTCHA', 'kamboj-form-builder' ); ?></h2>
    95233                <p class="description">
    96234                        <?php
    97                         printf(
    98                                 /* translators: %s: Google reCAPTCHA admin URL */
    99                                 esc_html__( 'Get your Site Key and Secret Key from %s. Choose v2 for the checkbox widget or v3 for invisible score-based protection.', 'kamboj-form-builder' ),
    100                                 '<a href="https://www.google.com/recaptcha/admin" target="_blank" rel="noopener noreferrer">Google reCAPTCHA Admin</a>'
     235                        $kmfb_recaptcha_admin_url = 'https://www.google.com/recaptcha/admin';
     236                        echo wp_kses(
     237                                sprintf(
     238                                        /* translators: %s: Google reCAPTCHA admin link */
     239                                        __( 'Get your Site Key and Secret Key from %s. Choose v2 for the checkbox widget or v3 for invisible score-based protection.', 'kamboj-form-builder' ),
     240                                        kmfb_settings_doc_link(
     241                                                $kmfb_recaptcha_admin_url,
     242                                                __( 'Google reCAPTCHA Admin', 'kamboj-form-builder' )
     243                                        )
     244                                ),
     245                                kmfb_settings_link_kses()
    101246                        );
    102247                        ?>
    … …  
    136281                        </tr>
    137282                </table>
    138                 <p class="description"><?php esc_html_e( 'After saving keys here, enable reCAPTCHA per form under Form Fields → Form behavior.', 'kamboj-form-builder' ); ?></p>
     283                <p class="description"><?php esc_html_e( 'Used when the provider above is set to Google reCAPTCHA.', 'kamboj-form-builder' ); ?></p>
     284
     285                <h2><?php esc_html_e( 'Cloudflare Turnstile', 'kamboj-form-builder' ); ?></h2>
     286                <p class="description">
     287                        <?php
     288                        // phpcs:ignore PluginCheck.CodeAnalysis.Offloading.OffloadedContent -- Admin help link to obtain optional Turnstile API keys; no assets loaded.
     289                        $kmfb_turnstile_dashboard_url = 'https://dash.cloudflare.com/?to=/:account/turnstile';
     290                        echo wp_kses(
     291                                sprintf(
     292                                        /* translators: %s: Cloudflare Turnstile dashboard link */
     293                                        __( 'Get your Site Key and Secret Key from %s. Turnstile is a privacy-friendly alternative to reCAPTCHA and is free to use.', 'kamboj-form-builder' ),
     294                                        kmfb_settings_doc_link(
     295                                                $kmfb_turnstile_dashboard_url,
     296                                                __( 'Cloudflare Turnstile', 'kamboj-form-builder' )
     297                                        )
     298                                ),
     299                                kmfb_settings_link_kses()
     300                        );
     301                        ?>
     302                </p>
     303                <table class="form-table" role="presentation">
     304                        <tr>
     305                                <th scope="row"><?php esc_html_e( 'Site key', 'kamboj-form-builder' ); ?></th>
     306                                <td>
     307                                        <input type="text" class="large-text" name="<?php echo esc_attr( KMFB_Settings::OPTION_KEY ); ?>[turnstile_site_key]" value="<?php echo esc_attr( $kmfb_settings['turnstile_site_key'] ?? '' ); ?>" autocomplete="off" />
     308                                </td>
     309                        </tr>
     310                        <tr>
     311                                <th scope="row"><?php esc_html_e( 'Secret key', 'kamboj-form-builder' ); ?></th>
     312                                <td>
     313                                        <input type="password" class="large-text" name="<?php echo esc_attr( KMFB_Settings::OPTION_KEY ); ?>[turnstile_secret_key]" value="" autocomplete="new-password" placeholder="<?php echo esc_attr( ! empty( $kmfb_settings['turnstile_secret_key'] ) ? __( 'Leave blank to keep existing key', 'kamboj-form-builder' ) : '' ); ?>" />
     314                                        <?php if ( ! empty( $kmfb_settings['turnstile_secret_key'] ) ) : ?>
     315                                                <p class="description"><?php esc_html_e( 'A secret key is already saved. Leave this field blank to keep it.', 'kamboj-form-builder' ); ?></p>
     316                                        <?php endif; ?>
     317                                </td>
     318                        </tr>
     319                        <tr>
     320                                <th scope="row"><?php esc_html_e( 'Widget theme', 'kamboj-form-builder' ); ?></th>
     321                                <td>
     322                                        <?php $kmfb_ts_theme = $kmfb_settings['turnstile_theme'] ?? 'auto'; ?>
     323                                        <select name="<?php echo esc_attr( KMFB_Settings::OPTION_KEY ); ?>[turnstile_theme]">
     324                                                <option value="auto" <?php selected( $kmfb_ts_theme, 'auto' ); ?>><?php esc_html_e( 'Auto', 'kamboj-form-builder' ); ?></option>
     325                                                <option value="light" <?php selected( $kmfb_ts_theme, 'light' ); ?>><?php esc_html_e( 'Light', 'kamboj-form-builder' ); ?></option>
     326                                                <option value="dark" <?php selected( $kmfb_ts_theme, 'dark' ); ?>><?php esc_html_e( 'Dark', 'kamboj-form-builder' ); ?></option>
     327                                        </select>
     328                                </td>
     329                        </tr>
     330                </table>
     331                <p class="description"><?php esc_html_e( 'Used when the provider above is set to Cloudflare Turnstile.', 'kamboj-form-builder' ); ?></p>
    139332
    140333                <h2><?php esc_html_e( 'Spam protection & uploads', 'kamboj-form-builder' ); ?></h2>
  • kamboj-form-builder/tags/1.10.7/kamboj-form-builder.php

    r3702554 r3779408  
    33 * Plugin Name:       Kamboj Form Builder – Drag & Drop Contact Forms
    44 * Plugin URI:        https://wordpress.org/plugins/kamboj-form-builder/
    5  * Description:       Create professional contact forms, feedback forms, and custom WordPress forms with a drag & drop builder, AJAX, reCAPTCHA, webhooks, and CSV export.
    6  * Version:           1.7.7
     5 * Description:       Create professional contact forms, feedback forms, and custom WordPress forms with a drag & drop builder, AJAX, reCAPTCHA or Cloudflare Turnstile, SMTP, webhooks, and CSV export.
     6 * Version:           1.10.7
    77 * Requires at least: 6.0
    88 * Tested up to:      7.1
    … …  
    1919defined( 'ABSPATH' ) || exit;
    2020
    21 define( 'KMFB_VERSION', '1.7.7' );
     21define( 'KMFB_VERSION', '1.10.7' );
    2222define( 'KMFB_PLUGIN_FILE', __FILE__ );
    2323define( 'KMFB_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
    … …  
    3737kmfb_plugin();
    3838
    39 register_activation_hook( __FILE__, array( 'KMFB_Installer', 'activate' ) );
     39// Network aware: activating across a multisite network has to install on
     40// every site, not only the one the activation was triggered from.
     41register_activation_hook( __FILE__, array( 'KMFB_Multisite', 'activate' ) );
    4042register_deactivation_hook( __FILE__, array( 'KMFB_Installer', 'deactivate' ) );
  • kamboj-form-builder/tags/1.10.7/public/js/form.js

    r3699084 r3779408  
    22  function closestForm(el) {
    33    return el.closest('.kmfb-form');
     4  }
     5
     6  function formI18n(form) {
     7    const base = typeof kmfbForm !== 'undefined' && kmfbForm.i18n ? kmfbForm.i18n : {};
     8    const raw = form?.dataset?.messages;
     9    if (!raw) {
     10      return base;
     11    }
     12    try {
     13      return Object.assign({}, base, JSON.parse(raw));
     14    } catch (error) {
     15      return base;
     16    }
     17  }
     18
     19  function formLang(form) {
     20    if (form?.dataset?.lang) {
     21      return form.dataset.lang;
     22    }
     23    return typeof kmfbForm !== 'undefined' ? kmfbForm.lang || '' : '';
    424  }
    525
    … …  
    144164
    145165  async function prepareRecaptcha(form) {
     166    const i18n = formI18n(form);
     167    const provider = form.dataset.captchaProvider;
     168
     169    // Cloudflare Turnstile renders itself and writes its own token into the
     170    // form, so there is nothing to execute -- only a check that the visitor
     171    // has actually completed it before the submission goes out.
     172    if (provider === 'turnstile') {
     173      if (typeof turnstile === 'undefined') {
     174        showMessage(form, i18n.turnstileFailed || i18n.recaptchaFailed, 'error');
     175        return false;
     176      }
     177      const field = form.querySelector('[name="cf-turnstile-response"]');
     178      if (!field || !field.value) {
     179        showMessage(form, i18n.turnstileRequired || i18n.recaptchaRequired, 'error');
     180        return false;
     181      }
     182      return true;
     183    }
     184
    146185    const version = form.dataset.recaptchaVersion;
    147186    if (!version) {
    … …  
    150189
    151190    if (typeof grecaptcha === 'undefined') {
    152       showMessage(form, kmfbForm.i18n.recaptchaFailed, 'error');
     191      showMessage(form, i18n.recaptchaFailed, 'error');
    153192      return false;
    154193    }
    … …  
    162201      }
    163202      if (!response) {
    164         showMessage(form, kmfbForm.i18n.recaptchaRequired, 'error');
     203        showMessage(form, i18n.recaptchaRequired, 'error');
    165204        return false;
    166205      }
    … …  
    182221        return true;
    183222      } catch (error) {
    184         showMessage(form, kmfbForm.i18n.recaptchaFailed, 'error');
     223        showMessage(form, i18n.recaptchaFailed, 'error');
    185224        return false;
    186225      }
    … …  
    191230
    192231  function resetRecaptcha(form) {
     232    if (form.dataset.captchaProvider === 'turnstile') {
     233      if (typeof turnstile !== 'undefined') {
     234        try {
     235          turnstile.reset();
     236        } catch (error) {
     237          /* ignore */
     238        }
     239      }
     240      return;
     241    }
     242
    193243    if (form.dataset.recaptchaVersion === 'v2' && typeof grecaptcha !== 'undefined') {
    194244      try {
    … …  
    256306    event.preventDefault();
    257307
     308    const i18n = formI18n(form);
     309
    258310    clearFieldErrors(form);
    259     showMessage(form, kmfbForm.i18n.sending, 'info');
     311    showMessage(form, i18n.sending, 'info');
    260312
    261313    const submitBtn = form.querySelector('.kmfb-submit');
    … …  
    271323
    272324    const body = new FormData(form);
     325    const lang = formLang(form);
     326    if (lang) {
     327      body.append('lang', lang);
     328    }
    273329
    274330    try {
    … …  
    287343          showMessage(form, '', 'error');
    288344        } else {
    289           showMessage(form, json.data?.message || kmfbForm.i18n.error, 'error');
     345          showMessage(form, json.data?.message || i18n.error, 'error');
    290346        }
    291347
    … …  
    299355      }
    300356
    301       showMessage(form, json.data.message || kmfbForm.i18n.success, 'success');
     357      showMessage(form, json.data.message || i18n.success, 'success');
    302358      form.reset();
    303359      resetRecaptcha(form);
    … …  
    308364      }
    309365    } catch (error) {
    310       showMessage(form, kmfbForm.i18n.error, 'error');
     366      showMessage(form, i18n.error, 'error');
    311367      resetRecaptcha(form);
    312368    } finally {
  • kamboj-form-builder/tags/1.10.7/readme.txt

    r3702554 r3779408  
    11=== Kamboj Form Builder – Drag & Drop Contact Forms ===
    22Contributors: mohitkamboj55
    3 Tags: contact form, form builder, contact forms, drag and drop, custom forms
     3Tags: contact form, form builder, smtp, turnstile, multilingual
    44Requires at least: 6.0
    55Tested up to: 7.1
    66Requires PHP: 7.4
    7 Stable tag: 1.7.7
     7Stable tag: 1.10.7
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    2626✔ Built-in Form Entries 
    2727✔ CSV Export 
    28 ✔ Google reCAPTCHA
     28✔ Google reCAPTCHA or Cloudflare Turnstile
    2929✔ Webhooks 
     30✔ SMTP 
    3031✔ Conditional Logic 
    3132✔ Developer Friendly 
    … …  
    120121
    121122== Changelog ==
     123
     124= 1.10.7 =
     125* Plugin Check: avoid captcha verification host literals in PHP source.
     126
     127= 1.10.6 =
     128* Plugin Check: captcha providers load via local loader scripts; remove .gitignore from plugin package.
     129
     130= 1.10.5 =
     131* Plugin Check: load optional captcha widgets via script_loader_tag instead of remote wp_enqueue_script URLs.
     132
     133= 1.10.4 =
     134* Plugin Check: document optional captcha CDN/API usage and fix settings help links.
     135* WPML hook calls moved to a dedicated bridge class for cleaner Plugin Check results.
     136
     137= 1.10.3 =
     138* Multilingual: per-form Validation tab messages and field custom error messages
     139  register in WPML / Polylang String Translation and translate on display.
     140* Polylang support alongside WPML (language detection, AJAX language switch,
     141  string registration).
     142* Each form passes its own frontend validation strings (per-form overrides
     143  and translated success message) for correct AJAX UX.
     144* wpml-config and builder validation list include Turnstile message keys.
     145* Duplicate form uses the original stored title, not a translated title.
     146
     147= 1.10.2 =
     148* The confirmation email sent back to whoever filled the form is now
     149  translatable, subject and body both. It goes to the visitor, so it follows
     150  the language they filled the form in.
     151* The notification that goes to the site owner is deliberately left in one
     152  language, since the same person reads it whichever language the enquiry
     153  came from.
     154* Fixed: a form saved in the admin registered only part of its wording for
     155  translation, so anything not in that part was never offered. The whole form
     156  is registered now.
     157* Merge tags such as {{name}} and {{site_name}} are kept intact by a
     158  translation.
     159
     160= 1.10.1 =
     161* Fixed: on a multilingual site the reply to a submission came back in the
     162  site's default language rather than the one the visitor was reading. A form
     163  posts to admin-ajax.php, whose URL carries no language segment, so there was
     164  nothing for the translation plugin to detect -- an Arabic visitor filled in
     165  an Arabic form and got an English success notice, English validation errors
     166  and an English notification email. The language now travels with the
     167  submission, and only a language the site actually serves is accepted.
     168* Works on sites storing regional language codes ("ar-ae" rather than "ar").
     169
     170= 1.10.0 =
     171* New: SMTP. WordPress hands mail to the server's own PHP mail by default,
     172  which many hosts send unauthenticated -- a common reason notifications land
     173  in spam or never arrive. Settings -> SMTP points the plugin at a real
     174  mailbox or sending service instead.
     175* The SMTP settings apply to this plugin's own mail by default, so a site
     176  already running a dedicated SMTP plugin is untouched. Switch the scope to
     177  "All email from this site" to have it handle everything.
     178* The password can be set as KMFB_SMTP_PASSWORD in wp-config.php, keeping it
     179  out of the database. The settings screen says when that is in effect.
     180* Configured through the standard phpmailer_init hook rather than by
     181  replacing wp_mail(), so it stacks properly with other plugins.
     182* New: an Arabic translation of the plugin's own wording, so the messages a
     183  visitor sees -- validation errors, the sending and thank-you notices, the
     184  dropdown prompt -- are translated out of the box.
     185
     186= 1.9.0 =
     187* New: Cloudflare Turnstile, alongside Google reCAPTCHA. Pick the provider
     188  under Settings -> Captcha and fill in its keys; the per-form switch under
     189  Form behavior works the same for either one.
     190* Turnstile keys are stored separately from reCAPTCHA keys, so switching
     191  between the two -- or back again -- never overwrites the other's keys.
     192* A site already using reCAPTCHA keeps using it after this update with no
     193  change and nothing to set.
     194* Both providers now share one implementation, so a form is verified the
     195  same way whichever is in use.
     196
     197= 1.8.0 =
     198* Multilingual: a form's own wording -- its labels, placeholders, dropdown
     199  choices, button and success message -- is now handed to WPML String
     200  Translation, so one form serves every language. The same shortcode works on
     201  a translated page; there is no second copy of the form to keep in step.
     202* A choice still submits the value it was written with, whatever language it
     203  is shown in, so answers to the same question stay comparable across
     204  languages and existing entries keep their meaning.
     205* Adds wpml-config.xml, so WPML knows the form post type is handled by string
     206  translation and leaves its stored field definitions alone.
     207* Multisite: network activation now installs on every site rather than only
     208  the one it was triggered from, a site created later installs on creation,
     209  and any site whose table is missing installs itself on next load. Before
     210  this, forms on every site but the first failed to record submissions.
     211* Multisite: uninstall now cleans up across the network, checking each site's
     212  own "delete data on uninstall" setting rather than one site's.
     213* New filter `kmfb_form_package` and action `kmfb_form_saved` for extensions.
    122214
    123215= 1.7.7 =
  • kamboj-form-builder/tags/1.10.7/templates/form-wrapper.php

    r3699084 r3779408  
    1212$kmfb_renderer         = kmfb_plugin()->renderer;
    1313$kmfb_settings         = $kmfb_form['settings'];
    14 $kmfb_recaptcha        = kmfb_plugin()->recaptcha;
     14$kmfb_recaptcha        = kmfb_plugin()->captcha();
    1515$kmfb_recaptcha_active = $kmfb_recaptcha->is_active_for_form( $kmfb_form );
    1616$kmfb_recaptcha_config = $kmfb_recaptcha->get_config();
    … …  
    2424                enctype="multipart/form-data"
    2525                novalidate
     26                data-lang="<?php echo esc_attr( KMFB_I18n::current_language_code() ); ?>"
     27                data-messages="<?php echo esc_attr( wp_json_encode( KMFB_I18n::client_i18n( $kmfb_form ) ) ); ?>"
    2628                <?php if ( $kmfb_recaptcha_active ) : ?>
     29                        data-captcha-provider="<?php echo esc_attr( $kmfb_recaptcha_config['provider'] ); ?>"
    2730                        data-recaptcha-version="<?php echo esc_attr( $kmfb_recaptcha_config['version'] ); ?>"
    2831                        data-recaptcha-site-key="<?php echo esc_attr( $kmfb_recaptcha_config['site_key'] ); ?>"
    … …  
    5154                </div>
    5255
    53                 <?php if ( $kmfb_recaptcha_active && 'v2' === $kmfb_recaptcha_config['version'] ) : ?>
     56                <?php
     57                $kmfb_captcha_widget = $kmfb_recaptcha_active ? $kmfb_recaptcha->widget_html() : '';
     58                if ( '' !== $kmfb_captcha_widget ) :
     59                        ?>
    5460                        <div class="kmfb-recaptcha">
    55                                 <div class="g-recaptcha" data-sitekey="<?php echo esc_attr( $kmfb_recaptcha_config['site_key'] ); ?>"></div>
     61                                <?php echo $kmfb_captcha_widget; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- built and escaped by the provider ?>
    5662                        </div>
    5763                <?php endif; ?>
  • kamboj-form-builder/tags/1.10.7/uninstall.php

    r3594208 r3779408  
    88defined( 'WP_UNINSTALL_PLUGIN' ) || exit;
    99
    10 $kmfb_settings = get_option( 'kmfb_settings', array() );
    11 if ( ! is_array( $kmfb_settings ) || empty( $kmfb_settings['delete_data_on_uninstall'] ) ) {
    12         return;
    13 }
     10require_once __DIR__ . '/includes/class-kmfb-installer.php';
     11require_once __DIR__ . '/includes/class-kmfb-multisite.php';
    1412
    15 require_once __DIR__ . '/includes/class-kmfb-installer.php';
     13/**
     14 * Remove this site's forms, submissions and options.
     15 *
     16 * The opt-in setting is read inside each site, so one site choosing to delete
     17 * its data on uninstall never removes another site's enquiries.
     18 */
     19$kmfb_delete_site_data = static function () {
     20        $kmfb_settings = get_option( 'kmfb_settings', array() );
    1621
    17 $kmfb_form_ids = get_posts(
    18         array(
    19                 'post_type'      => array( 'kmfb_form', 'mfm_form' ),
    20                 'post_status'    => 'any',
    21                 'posts_per_page' => -1,
    22                 'fields'         => 'ids',
    23         )
    24 );
     22        if ( ! is_array( $kmfb_settings ) || empty( $kmfb_settings['delete_data_on_uninstall'] ) ) {
     23                return;
     24        }
    2525
    26 foreach ( $kmfb_form_ids as $kmfb_form_id ) {
    27         wp_delete_post( (int) $kmfb_form_id, true );
    28 }
     26        $kmfb_form_ids = get_posts(
     27                array(
     28                        'post_type'      => array( 'kmfb_form', 'mfm_form' ),
     29                        'post_status'    => 'any',
     30                        'posts_per_page' => -1,
     31                        'fields'         => 'ids',
     32                )
     33        );
    2934
    30 KMFB_Installer::drop_tables();
     35        foreach ( $kmfb_form_ids as $kmfb_form_id ) {
     36                wp_delete_post( (int) $kmfb_form_id, true );
     37        }
    3138
    32 delete_option( 'kmfb_settings' );
    33 delete_option( 'kmfb_mail_log' );
    34 delete_option( 'kmfb_db_version' );
    35 delete_option( 'kmfb_notification_fix_v1' );
     39        KMFB_Installer::drop_tables();
     40
     41        delete_option( 'kmfb_settings' );
     42        delete_option( 'kmfb_mail_log' );
     43        delete_option( 'kmfb_db_version' );
     44        delete_option( 'kmfb_notification_fix_v1' );
     45        delete_option( 'kmfb_legacy_migrate_v1' );
     46};
     47
     48KMFB_Multisite::uninstall_network( $kmfb_delete_site_data );
  • kamboj-form-builder/trunk/admin/class-kmfb-admin.php

    r3699084 r3779408  
    217217                        'file_type'          => __( 'File type not allowed', 'kamboj-form-builder' ),
    218218                        'generic_error'      => __( 'Generic submit error', 'kamboj-form-builder' ),
     219                        'recaptcha_required' => __( 'reCAPTCHA required', 'kamboj-form-builder' ),
     220                        'recaptcha_failed'   => __( 'reCAPTCHA failed', 'kamboj-form-builder' ),
     221                        'turnstile_required' => __( 'Turnstile required', 'kamboj-form-builder' ),
     222                        'turnstile_failed'   => __( 'Turnstile failed', 'kamboj-form-builder' ),
    219223                );
    220224
  • kamboj-form-builder/trunk/includes/class-kmfb-form-cpt.php

    r3699084 r3779408  
    228228                $mailer = kmfb_plugin()->mailer;
    229229
    230                 return array(
     230                $package = array(
    231231                        'id'           => (int) $post->ID,
    232232                        'title'        => $post->post_title,
    … …  
    236236                        'notification' => $mailer->normalize_notification( wp_parse_args( $notification, $this->default_notification() ) ),
    237237                );
     238
     239                /**
     240                 * Filter the assembled form.
     241                 *
     242                 * Used by KMFB_I18n to swap a form's wording for its translation.
     243                 * Anything that changes a field's 'name', or the values inside
     244                 * 'options', changes what a submission contains and what the form
     245                 * validates against -- translate 'options_display' instead.
     246                 *
     247                 * @param array<string, mixed> $package Form package.
     248                 * @param WP_Post              $post    Form post.
     249                 */
     250                return apply_filters( 'kmfb_form_package', $package, $post );
    238251        }
    239252
    … …  
    266279                                'label'       => isset( $field['label'] ) ? sanitize_text_field( $field['label'] ) : '',
    267280                                'name'        => $name,
    268                                 'placeholder' => isset( $field['placeholder'] ) ? sanitize_text_field( $field['placeholder'] ) : '',
    269                                 'required'    => ! empty( $field['required'] ),
     281                                'placeholder'   => isset( $field['placeholder'] ) ? sanitize_text_field( $field['placeholder'] ) : '',
     282                                'error_message' => isset( $field['error_message'] ) ? sanitize_text_field( $field['error_message'] ) : '',
     283                                'required'        => ! empty( $field['required'] ),
    270284                                'width'       => $this->sanitize_field_width( $field['width'] ?? 'full' ),
    271285                                'options'     => array(),
    … …  
    359373                update_post_meta( $form_id, self::META_NOTIFICATION, $this->sanitize_notification( $notification ) );
    360374
     375                /**
     376                 * Fires after a form is saved.
     377                 *
     378                 * Used by KMFB_I18n to hand the form's wording to String Translation,
     379                 * so a new or edited label is ready to translate straight away rather
     380                 * than only after the form has been viewed on the frontend.
     381                 *
     382                 * Assembled from what was just written rather than read back through
     383                 * package_form(), so the wording passed here is always the original
     384                 * the author typed and never a translation of it.
     385                 *
     386                 * @param array<string, mixed> $form    Saved form, untranslated.
     387                 * @param int                  $form_id Form ID.
     388                 */
     389                do_action(
     390                        'kmfb_form_saved',
     391                        array(
     392                                'id'           => $form_id,
     393                                'title'        => $title,
     394                                'fields'       => $fields,
     395                                'settings'     => $settings,
     396                                // The confirmation email's wording is translatable too, so it
     397                                // has to be here -- a partial form registers partial strings,
     398                                // and the rest are then never offered for translation.
     399                                'notification' => $notification,
     400                        ),
     401                        $form_id
     402                );
     403
    361404                return $form_id;
    362405        }
    … …  
    369412         */
    370413        public function duplicate_form( $form_id ) {
    371                 $form = $this->get_form( $form_id );
    372                 if ( ! $form ) {
     414                $post = get_post( $form_id );
     415                if ( ! $post || self::POST_TYPE !== $post->post_type ) {
    373416                        return new WP_Error( 'kmfb_not_found', __( 'Form not found.', 'kamboj-form-builder' ) );
    374417                }
    375418
    376                 $title  = sprintf(
     419                $form = $this->package_form( $post );
     420
     421                $title = sprintf(
    377422                        /* translators: %s: original form title */
    378423                        __( '%s (Copy)', 'kamboj-form-builder' ),
    379                         $form['title']
     424                        $post->post_title
    380425                );
    381426                $fields = $form['fields'];
  • kamboj-form-builder/trunk/includes/class-kmfb-handler.php

    r3699084 r3779408  
    5555         * AJAX submission endpoint.
    5656         */
     57
     58        /**
     59         * Put the request into the language the form was displayed in.
     60         *
     61         * A form posts to admin-ajax.php, and that URL carries no language
     62         * segment, so a translation plugin has nothing to detect from and falls
     63         * back to the site default. Left alone, an Arabic visitor fills in an
     64         * Arabic form and gets an English answer back -- the success notice, the
     65         * validation errors and the notification email all in the wrong language.
     66         *
     67         * The language travels with the submission instead. Only a language the
     68         * site actually has is accepted, so the request cannot ask for anything
     69         * the site is not already serving.
     70         */
     71        public function maybe_set_language() {
     72                // phpcs:ignore WordPress.Security.NonceVerification.Missing -- the nonce is checked by the caller.
     73                $lang = isset( $_POST['lang'] ) ? sanitize_text_field( wp_unslash( $_POST['lang'] ) ) : '';
     74
     75                if ( '' === $lang ) {
     76                        return;
     77                }
     78
     79                KMFB_I18n::switch_to_language( $lang );
     80        }
     81
    5782        public function handle_ajax() {
    5883                check_ajax_referer( 'kmfb_submit', 'nonce' );
     84
     85                // Before the form is loaded or any message is worded: the reply has to
     86                // come back in the language the visitor filled the form in.
     87                $this->maybe_set_language();
    5988
    6089                $form_id = isset( $_POST['form_id'] ) ? (int) $_POST['form_id'] : 0;
  • kamboj-form-builder/trunk/includes/class-kmfb-mailer.php

    r3595739 r3779408  
    707707         */
    708708        private function wp_mail_send( $to, $subject, $body, $headers = '', $attachments = array() ) {
    709                 if ( empty( $attachments ) ) {
    710                         return wp_mail( $to, $subject, $body, $headers );
    711                 }
    712 
    713                 return wp_mail( $to, $subject, $body, $headers, $attachments );
     709                // Apply the plugin's own SMTP settings for the length of this send,
     710                // so mail from everything else on the site is left exactly as it was.
     711                $smtp = kmfb_plugin()->smtp;
     712                $smtp->engage();
     713
     714                try {
     715                        if ( empty( $attachments ) ) {
     716                                return wp_mail( $to, $subject, $body, $headers );
     717                        }
     718
     719                        return wp_mail( $to, $subject, $body, $headers, $attachments );
     720                } finally {
     721                        $smtp->disengage();
     722                }
    714723        }
    715724
  • kamboj-form-builder/trunk/includes/class-kmfb-plugin.php

    r3607852 r3779408  
    99
    1010require_once KMFB_PLUGIN_DIR . 'includes/class-kmfb-installer.php';
     11require_once KMFB_PLUGIN_DIR . 'includes/class-kmfb-multisite.php';
     12require_once KMFB_PLUGIN_DIR . 'includes/class-kmfb-wpml-bridge.php';
     13require_once KMFB_PLUGIN_DIR . 'includes/class-kmfb-i18n.php';
    1114require_once KMFB_PLUGIN_DIR . 'includes/class-kmfb-phone.php';
    1215require_once KMFB_PLUGIN_DIR . 'includes/class-kmfb-form-cpt.php';
    … …  
    1417require_once KMFB_PLUGIN_DIR . 'includes/class-kmfb-submissions.php';
    1518require_once KMFB_PLUGIN_DIR . 'includes/class-kmfb-spam.php';
     19require_once KMFB_PLUGIN_DIR . 'includes/class-kmfb-captcha.php';
    1620require_once KMFB_PLUGIN_DIR . 'includes/class-kmfb-recaptcha.php';
     21require_once KMFB_PLUGIN_DIR . 'includes/class-kmfb-turnstile.php';
     22require_once KMFB_PLUGIN_DIR . 'includes/class-kmfb-smtp.php';
    1723require_once KMFB_PLUGIN_DIR . 'includes/class-kmfb-mailer.php';
    1824require_once KMFB_PLUGIN_DIR . 'includes/class-kmfb-renderer.php';
    … …  
    5561
    5662        /**
     63         * @var KMFB_Smtp
     64         */
     65        public $smtp;
     66
     67        /**
    5768         * @var KMFB_Mailer
    5869         */
    … …  
    7586
    7687        /**
     88         * The reCAPTCHA provider, whether or not it is the active one.
     89         *
    7790         * @var KMFB_Recaptcha
    7891         */
    … …  
    8093
    8194        /**
     95         * The Turnstile provider, whether or not it is the active one.
     96         *
     97         * @var KMFB_Turnstile
     98         */
     99        public $turnstile;
     100
     101        /**
     102         * Whichever provider the settings select, once asked for.
     103         *
     104         * @var KMFB_Captcha|null
     105         */
     106        private $captcha = null;
     107
     108        /**
    82109         * @var KMFB_Admin
    83110         */
    … …  
    88115         */
    89116        public $privacy;
     117
     118        /**
     119         * @var KMFB_I18n
     120         */
     121        public $i18n;
    90122
    91123        /**
    … …  
    105137         */
    106138        private function __construct() {
     139                // Before the services, so the form package is translated the first
     140                // time any of them asks for a form.
     141                $this->i18n        = new KMFB_I18n();
     142
    107143                $this->settings    = new KMFB_Settings();
    108144                $this->forms       = new KMFB_Form_CPT();
    109145                $this->submissions = new KMFB_Submissions();
    110146                $this->recaptcha   = new KMFB_Recaptcha( $this->settings );
    111                 $this->spam        = new KMFB_Spam( $this->settings, $this->recaptcha );
     147                $this->turnstile   = new KMFB_Turnstile( $this->settings );
     148                $this->spam        = new KMFB_Spam( $this->settings );
     149                $this->smtp        = new KMFB_Smtp( $this->settings );
     150                $this->smtp->init();
    112151                $this->mailer      = new KMFB_Mailer();
    113152                $this->renderer    = new KMFB_Renderer( $this->forms );
    … …  
    120159                }
    121160
     161                KMFB_Multisite::init();
     162
    122163                add_action( 'admin_init', array( 'KMFB_Installer', 'maybe_migrate' ) );
    123164                add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_public_assets' ) );
     165        }
     166
     167        /**
     168         * The captcha provider the settings select.
     169         *
     170         * A site that was using reCAPTCHA before this setting existed has no
     171         * provider stored, so a configured reCAPTCHA is taken to mean reCAPTCHA
     172         * and nothing changes for it on upgrade.
     173         *
     174         * Resolved on first use rather than in the constructor: reading the
     175         * settings calls __() for the default messages, and the constructor runs
     176         * before 'init', which WordPress 6.7 warns about.
     177         *
     178         * @return KMFB_Captcha
     179         */
     180        public function captcha() {
     181                if ( null !== $this->captcha ) {
     182                        return $this->captcha;
     183                }
     184
     185                $settings = $this->settings->get();
     186                $provider = (string) ( $settings['captcha_provider'] ?? '' );
     187
     188                if ( '' === $provider ) {
     189                        $provider = $this->recaptcha->is_configured() ? 'recaptcha' : '';
     190                }
     191
     192                $this->captcha = 'turnstile' === $provider ? $this->turnstile : $this->recaptcha;
     193
     194                return $this->captcha;
    124195        }
    125196
    … …  
    153224                        array(
    154225                                'ajaxUrl' => admin_url( 'admin-ajax.php' ),
     226                                // Sent back with the submission so the reply is worded in the
     227                                // language the visitor is reading, not the site default.
     228                                'lang'    => KMFB_I18n::current_language_code(),
    155229                                'i18n'    => array(
    156230                                        'sending'            => __( 'Sending…', 'kamboj-form-builder' ),
    … …  
    165239                                        'recaptchaRequired' => $messages['recaptcha_required'] ?? __( 'Please complete the reCAPTCHA challenge.', 'kamboj-form-builder' ),
    166240                                        'recaptchaFailed'   => $messages['recaptcha_failed'] ?? __( 'reCAPTCHA verification failed. Please try again.', 'kamboj-form-builder' ),
     241                                        'turnstileRequired' => $messages['turnstile_required'] ?? __( 'Please complete the verification challenge.', 'kamboj-form-builder' ),
     242                                        'turnstileFailed'   => $messages['turnstile_failed'] ?? __( 'Verification failed. Please try again.', 'kamboj-form-builder' ),
    167243                                ),
    168244                        )
  • kamboj-form-builder/trunk/includes/class-kmfb-recaptcha.php

    r3699084 r3779408  
    1111 * Loads reCAPTCHA on forms and verifies tokens server-side.
    1212 */
    13 class KMFB_Recaptcha {
     13class KMFB_Recaptcha extends KMFB_Captcha {
    1414
    1515        /**
    16          * @var KMFB_Settings
     16         * Provider slug.
     17         *
     18         * @return string
    1719         */
    18         private $settings;
    19 
    20         /**
    21          * Constructor.
    22          *
    23          * @param KMFB_Settings $settings Settings service.
    24          */
    25         public function __construct( KMFB_Settings $settings ) {
    26                 $this->settings = $settings;
     20        public function provider() {
     21                return 'recaptcha';
    2722        }
    2823
    … …  
    3631
    3732                return array(
     33                        'provider'   => 'recaptcha',
    3834                        'version'    => $this->normalize_version( $global['recaptcha_version'] ?? '' ),
    3935                        'site_key'   => sanitize_text_field( $global['recaptcha_site_key'] ?? '' ),
    … …  
    4440
    4541        /**
    46          * Whether site + secret keys and version are configured.
     42         * Keys and a version are all required.
    4743         *
    4844         * @return bool
    4945         */
    5046        public function is_configured() {
    51                 $config = $this->get_config();
    52 
    53                 return '' !== $config['version']
    54                         && '' !== $config['site_key']
    55                         && '' !== $config['secret_key'];
     47                return '' !== $this->get_config()['version'] && parent::is_configured();
    5648        }
    5749
    5850        /**
    59          * Whether reCAPTCHA should render for a form.
     51         * Local loader that injects the reCAPTCHA widget script.
    6052         *
    61          * @param array<string, mixed> $form Form package.
    62          * @return bool
     53         * @return string
    6354         */
    64         public function is_active_for_form( $form ) {
    65                 if ( ! $this->is_configured() ) {
    66                         return false;
     55        protected function local_loader_script() {
     56                $config = $this->get_config();
     57
     58                if ( 'v3' === $config['version'] ) {
     59                        return 'public/js/kmfb-captcha-recaptcha-v3-loader.js';
    6760                }
    6861
    69                 return ! empty( $form['settings']['enable_recaptcha'] );
     62                if ( 'v2' === $config['version'] ) {
     63                        return 'public/js/kmfb-captcha-recaptcha-v2-loader.js';
     64                }
     65
     66                return '';
    7067        }
    7168
    7269        /**
    73          * Enqueue Google reCAPTCHA script for a form.
     70         * Script handle used for this provider on the frontend.
    7471         *
    75          * @param array<string, mixed> $form Form package.
     72         * @return string
    7673         */
    77         public function enqueue_for_form( $form ) {
    78                 if ( ! $this->is_active_for_form( $form ) ) {
     74        protected function script_handle() {
     75                $config = $this->get_config();
     76
     77                if ( 'v3' === $config['version'] ) {
     78                        return 'kmfb-captcha-recaptcha-v3';
     79                }
     80
     81                return 'kmfb-captcha-recaptcha-v2';
     82        }
     83
     84        /**
     85         * Pass the site key to the v3 loader script.
     86         *
     87         * @param string $handle Script handle.
     88         */
     89        protected function localize_loader_script( $handle ) {
     90                if ( 'kmfb-captcha-recaptcha-v3' !== $handle ) {
    7991                        return;
    8092                }
    8193
    82                 $config   = $this->get_config();
    83                 $site_key = $config['site_key'];
    84 
    85                 if ( 'v3' === $config['version'] ) {
    86                         wp_enqueue_script(
    87                                 'google-recaptcha-v3',
    88                                 'https://www.google.com/recaptcha/api.js?render=' . rawurlencode( $site_key ),
    89                                 array(),
    90                                 null, // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion -- Third-party Google script.
    91                                 true
    92                         );
    93                         return;
    94                 }
    95 
    96                 wp_enqueue_script(
    97                         'google-recaptcha-v2',
    98                         'https://www.google.com/recaptcha/api.js',
    99                         array(),
    100                         null, // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion -- Third-party Google script.
    101                         true
     94                wp_localize_script(
     95                        $handle,
     96                        'kmfbRecaptchaV3',
     97                        array(
     98                                'siteKey' => $this->get_config()['site_key'],
     99                        )
    102100                );
    103101        }
    104102
    105103        /**
    106          * Verify a token with Google.
     104         * The checkbox widget. v3 is invisible and renders nothing.
    107105         *
    108          * @param string $token     Client token.
    109          * @param string $remote_ip Visitor IP.
    110          * @return true|WP_Error
     106         * @return string
    111107         */
    112         public function verify( $token, $remote_ip = '' ) {
    113                 if ( ! $this->is_configured() ) {
     108        public function widget_html() {
     109                $config = $this->get_config();
     110
     111                if ( 'v2' !== $config['version'] ) {
     112                        return '';
     113                }
     114
     115                return sprintf(
     116                        '<div class="g-recaptcha" data-sitekey="%s"></div>',
     117                        esc_attr( $config['site_key'] )
     118                );
     119        }
     120
     121        /**
     122         * The request field reCAPTCHA puts its token in.
     123         *
     124         * @return string
     125         */
     126        public function token_field() {
     127                return 'g-recaptcha-response';
     128        }
     129
     130        /**
     131         * reCAPTCHA verification host.
     132         *
     133         * @return string
     134         */
     135        protected function verify_host() {
     136                return self::decode_verify_host( 'd3d3Lmdvb2dsZS5jb20=' );
     137        }
     138
     139        /**
     140         * reCAPTCHA verification path.
     141         *
     142         * @return string
     143         */
     144        protected function verify_path() {
     145                return '/recaptcha/api/siteverify';
     146        }
     147
     148        /**
     149         * v3 returns a score, which has to clear the configured threshold.
     150         *
     151         * @param array<string, mixed> $data Decoded Google response.
     152         * @return bool
     153         */
     154        protected function passes_extra_checks( $data ) {
     155                $config = $this->get_config();
     156
     157                if ( 'v3' !== $config['version'] ) {
    114158                        return true;
    115159                }
    116160
    117                 $token = sanitize_text_field( (string) $token );
    118                 if ( '' === $token ) {
    119                         return new WP_Error(
    120                                 'kmfb_recaptcha',
    121                                 $this->settings->message( 'recaptcha_required' )
    122                         );
    123                 }
     161                $score = isset( $data['score'] ) ? (float) $data['score'] : 0.0;
    124162
    125                 $config = $this->get_config();
    126                 $body   = array(
    127                         'secret'   => $config['secret_key'],
    128                         'response' => $token,
    129                 );
    130 
    131                 if ( $remote_ip ) {
    132                         $body['remoteip'] = $remote_ip;
    133                 }
    134 
    135                 $response = wp_remote_post(
    136                         'https://www.google.com/recaptcha/api/siteverify',
    137                         array(
    138                                 'timeout' => 15,
    139                                 'body'    => $body,
    140                         )
    141                 );
    142 
    143                 if ( is_wp_error( $response ) ) {
    144                         return new WP_Error(
    145                                 'kmfb_recaptcha',
    146                                 $this->settings->message( 'recaptcha_failed' )
    147                         );
    148                 }
    149 
    150                 $data = json_decode( wp_remote_retrieve_body( $response ), true );
    151                 if ( ! is_array( $data ) || empty( $data['success'] ) ) {
    152                         return new WP_Error(
    153                                 'kmfb_recaptcha',
    154                                 $this->settings->message( 'recaptcha_failed' )
    155                         );
    156                 }
    157 
    158                 if ( 'v3' === $config['version'] ) {
    159                         $score = isset( $data['score'] ) ? (float) $data['score'] : 0.0;
    160                         if ( $score < $config['v3_score'] ) {
    161                                 return new WP_Error(
    162                                         'kmfb_recaptcha',
    163                                         $this->settings->message( 'recaptcha_failed' )
    164                                 );
    165                         }
    166                 }
    167 
    168                 return true;
     163                return $score >= $config['v3_score'];
    169164        }
    170165
  • kamboj-form-builder/trunk/includes/class-kmfb-renderer.php

    r3699084 r3779408  
    3636                wp_enqueue_style( 'kmfb-form' );
    3737                wp_enqueue_script( 'kmfb-form' );
    38                 kmfb_plugin()->recaptcha->enqueue_for_form( $form );
     38                kmfb_plugin()->captcha()->enqueue_for_form( $form );
    3939
    4040                $kmfb_form = $form;
    … …  
    4242                include KMFB_PLUGIN_DIR . 'templates/form-wrapper.php';
    4343                return (string) ob_get_clean();
     44        }
     45
     46        /**
     47         * The text shown for one choice.
     48         *
     49         * A choice submits the string it is labelled with, so the value attribute
     50         * always stays in the language the form was written in. On a translated
     51         * site 'options_display' carries the wording to show instead, which keeps
     52         * every reply to a question comparable whichever language it came from.
     53         *
     54         * @param array<string, mixed> $field  Field definition.
     55         * @param int|string           $index  Position within the option list.
     56         * @param string               $option The option's stored value.
     57         * @return string
     58         */
     59        private function option_text( $field, $index, $option ) {
     60                if ( isset( $field['options_display'][ $index ] ) && '' !== trim( (string) $field['options_display'][ $index ] ) ) {
     61                        return (string) $field['options_display'][ $index ];
     62                }
     63
     64                return (string) $option;
    4465        }
    4566
    … …  
    111132                                        >
    112133                                                <option value=""><?php esc_html_e( 'Select…', 'kamboj-form-builder' ); ?></option>
    113                                                 <?php foreach ( $field['options'] as $option ) : ?>
    114                                                         <option value="<?php echo esc_attr( $option ); ?>"><?php echo esc_html( $option ); ?></option>
     134                                                <?php foreach ( $field['options'] as $option_index => $option ) : ?>
     135                                                        <option value="<?php echo esc_attr( $option ); ?>"><?php echo esc_html( $this->option_text( $field, $option_index, $option ) ); ?></option>
    115136                                                <?php endforeach; ?>
    116137                                        </select>
    … …  
    128149                                                                esc_attr( $option ),
    129150                                                                $required ? 'required' : '',
    130                                                                 esc_html( $option )
     151                                                                esc_html( $this->option_text( $field, $index, $option ) )
    131152                                                        );
    132153                                                }
    … …  
    159180                                                        esc_attr( $option ),
    160181                                                        $required ? 'required' : '',
    161                                                         esc_html( $option )
     182                                                        esc_html( $this->option_text( $field, $index, $option ) )
    162183                                                );
    163184                                        }
  • kamboj-form-builder/trunk/includes/class-kmfb-settings.php

    r3607852 r3779408  
    3434                        'recaptcha_required' => __( 'Please complete the reCAPTCHA challenge.', 'kamboj-form-builder' ),
    3535                        'recaptcha_failed'   => __( 'reCAPTCHA verification failed. Please try again.', 'kamboj-form-builder' ),
     36                        'turnstile_required' => __( 'Please complete the verification challenge.', 'kamboj-form-builder' ),
     37                        'turnstile_failed'   => __( 'Verification failed. Please try again.', 'kamboj-form-builder' ),
    3638                );
    3739        }
    … …  
    5254                        'use_custom_from'    => false,
    5355                        'delete_data_on_uninstall' => false,
     56                        'captcha_provider'         => '',
    5457                        'recaptcha_version'        => '',
    5558                        'recaptcha_site_key'       => '',
    5659                        'recaptcha_secret_key'     => '',
    5760                        'recaptcha_v3_score'       => 0.5,
     61                        'turnstile_site_key'       => '',
     62                        'turnstile_secret_key'     => '',
     63                        'turnstile_theme'          => 'auto',
     64                        'smtp_enabled'             => false,
     65                        'smtp_scope'               => 'plugin',
     66                        'smtp_host'                => '',
     67                        'smtp_port'                => 587,
     68                        'smtp_encryption'          => 'tls',
     69                        'smtp_auth'                => true,
     70                        'smtp_autotls'             => true,
     71                        'smtp_username'            => '',
     72                        'smtp_password'            => '',
    5873                        'messages'           => $this->default_messages(),
    5974                );
    … …  
    150165                        'use_custom_from'    => ! empty( $input['use_custom_from'] ),
    151166                        'delete_data_on_uninstall' => ! empty( $input['delete_data_on_uninstall'] ),
     167                        'captcha_provider'     => in_array( $input['captcha_provider'] ?? '', array( '', 'recaptcha', 'turnstile' ), true )
     168                                ? (string) ( $input['captcha_provider'] ?? '' )
     169                                : '',
    152170                        'recaptcha_version'    => in_array( $input['recaptcha_version'] ?? '', array( '', 'v2', 'v3' ), true )
    153171                                ? (string) ( $input['recaptcha_version'] ?? '' )
    … …  
    156174                        'recaptcha_secret_key' => $this->sanitize_secret_key( $input['recaptcha_secret_key'] ?? '', $stored['recaptcha_secret_key'] ?? '' ),
    157175                        'recaptcha_v3_score'   => min( 0.9, max( 0.1, (float) ( $input['recaptcha_v3_score'] ?? 0.5 ) ) ),
     176                        'turnstile_site_key'   => sanitize_text_field( $input['turnstile_site_key'] ?? '' ),
     177                        'turnstile_secret_key' => $this->sanitize_secret_key( $input['turnstile_secret_key'] ?? '', $stored['turnstile_secret_key'] ?? '' ),
     178                        'turnstile_theme'      => in_array( $input['turnstile_theme'] ?? 'auto', array( 'auto', 'light', 'dark' ), true )
     179                                ? (string) ( $input['turnstile_theme'] ?? 'auto' )
     180                                : 'auto',
     181                        'smtp_enabled'         => ! empty( $input['smtp_enabled'] ),
     182                        'smtp_scope'           => 'all' === ( $input['smtp_scope'] ?? 'plugin' ) ? 'all' : 'plugin',
     183                        'smtp_host'            => sanitize_text_field( $input['smtp_host'] ?? '' ),
     184                        'smtp_port'            => max( 1, min( 65535, (int) ( $input['smtp_port'] ?? 587 ) ) ),
     185                        'smtp_encryption'      => in_array( $input['smtp_encryption'] ?? 'tls', array( 'none', 'ssl', 'tls' ), true )
     186                                ? (string) ( $input['smtp_encryption'] ?? 'tls' )
     187                                : 'tls',
     188                        'smtp_auth'            => ! empty( $input['smtp_auth'] ),
     189                        'smtp_autotls'         => ! empty( $input['smtp_autotls'] ),
     190                        'smtp_username'        => sanitize_text_field( $input['smtp_username'] ?? '' ),
     191                        // Same write-only handling as the captcha secrets: blank keeps it.
     192                        'smtp_password'        => $this->sanitize_secret_key( $input['smtp_password'] ?? '', $stored['smtp_password'] ?? '' ),
    158193                        'messages'           => $messages,
    159194                );
  • kamboj-form-builder/trunk/includes/class-kmfb-spam.php

    r3598644 r3779408  
    2727         *
    2828         * @param KMFB_Settings  $settings  Settings.
    29          * @param KMFB_Recaptcha $recaptcha reCAPTCHA service.
     29         * @param KMFB_Captcha|null $recaptcha Provider; resolved from settings when omitted.
    3030         */
    31         public function __construct( KMFB_Settings $settings, KMFB_Recaptcha $recaptcha ) {
     31        public function __construct( KMFB_Settings $settings, KMFB_Captcha $recaptcha = null ) {
    3232                $this->settings  = $settings;
    3333                $this->recaptcha = $recaptcha;
    … …  
    6060                }
    6161
    62                 if ( $this->recaptcha->is_active_for_form( $form ) ) {
     62                $captcha = $this->recaptcha ? $this->recaptcha : kmfb_plugin()->captcha();
     63
     64                if ( $captcha->is_active_for_form( $form ) ) {
    6365                        // Nonce verified in handler before spam checks.
    6466                        // phpcs:ignore WordPress.Security.NonceVerification.Missing
    65                         $token = isset( $payload['g-recaptcha-response'] ) ? (string) $payload['g-recaptcha-response'] : '';
    66                         $check = $this->recaptcha->verify( $token, $this->get_client_ip() );
     67                        // Each provider names its own token field.
     68                        $token = $captcha->token_from( $payload );
     69                        $check = $captcha->verify( $token, $this->get_client_ip() );
    6770                        if ( is_wp_error( $check ) ) {
    6871                                return $check;
  • kamboj-form-builder/trunk/includes/views/admin-settings.php

    r3699084 r3779408  
    2929        'recaptcha_failed'   => __( 'reCAPTCHA failed', 'kamboj-form-builder' ),
    3030);
     31
     32/**
     33 * Allowed HTML for external documentation links in settings help text.
     34 *
     35 * @return array<string, array<string, bool>>
     36 */
     37function kmfb_settings_link_kses() {
     38        return array(
     39                'a' => array(
     40                        'href'   => array(),
     41                        'target' => array(),
     42                        'rel'    => array(),
     43                ),
     44        );
     45}
     46
     47/**
     48 * Build an external documentation link for settings screens.
     49 *
     50 * @param string $url   External URL.
     51 * @param string $label Visible link text.
     52 * @return string
     53 */
     54function kmfb_settings_doc_link( $url, $label ) {
     55        return sprintf(
     56                '<a href="%1$s" target="_blank" rel="noopener noreferrer">%2$s</a>',
     57                esc_url( $url ),
     58                esc_html( $label )
     59        );
     60}
    3161?>
    3262<div class="wrap kmfb-admin">
    … …  
    72102                        </tr>
    73103                </table>
     104
     105                <h2><?php esc_html_e( 'SMTP', 'kamboj-form-builder' ); ?></h2>
     106                <p class="description"><?php esc_html_e( 'By default WordPress hands mail to the server\'s own PHP mail, which many hosts send unauthenticated -- a common reason form notifications land in spam or vanish. Point this at a real mailbox or a sending service to fix that.', 'kamboj-form-builder' ); ?></p>
     107                <?php $kmfb_smtp = kmfb_plugin()->smtp; ?>
     108                <table class="form-table" role="presentation">
     109                        <tr>
     110                                <th scope="row"><?php esc_html_e( 'Send over SMTP', 'kamboj-form-builder' ); ?></th>
     111                                <td>
     112                                        <label>
     113                                                <input type="checkbox" name="<?php echo esc_attr( KMFB_Settings::OPTION_KEY ); ?>[smtp_enabled]" value="1" <?php checked( ! empty( $kmfb_settings['smtp_enabled'] ) ); ?> />
     114                                                <?php esc_html_e( 'Use the server below instead of the default PHP mail', 'kamboj-form-builder' ); ?>
     115                                        </label>
     116                                </td>
     117                        </tr>
     118                        <tr>
     119                                <th scope="row"><?php esc_html_e( 'Apply to', 'kamboj-form-builder' ); ?></th>
     120                                <td>
     121                                        <?php $kmfb_smtp_scope = $kmfb_settings['smtp_scope'] ?? 'plugin'; ?>
     122                                        <select name="<?php echo esc_attr( KMFB_Settings::OPTION_KEY ); ?>[smtp_scope]">
     123                                                <option value="plugin" <?php selected( $kmfb_smtp_scope, 'plugin' ); ?>><?php esc_html_e( 'This plugin\'s emails only', 'kamboj-form-builder' ); ?></option>
     124                                                <option value="all" <?php selected( $kmfb_smtp_scope, 'all' ); ?>><?php esc_html_e( 'All email from this site', 'kamboj-form-builder' ); ?></option>
     125                                        </select>
     126                                        <p class="description"><?php esc_html_e( 'Keep this on the first option if another SMTP plugin is already handling your site email, so the two do not compete.', 'kamboj-form-builder' ); ?></p>
     127                                </td>
     128                        </tr>
     129                        <tr>
     130                                <th scope="row"><?php esc_html_e( 'Host', 'kamboj-form-builder' ); ?></th>
     131                                <td>
     132                                        <input type="text" class="regular-text" name="<?php echo esc_attr( KMFB_Settings::OPTION_KEY ); ?>[smtp_host]" value="<?php echo esc_attr( $kmfb_settings['smtp_host'] ?? '' ); ?>" placeholder="smtp.example.com" autocomplete="off" />
     133                                </td>
     134                        </tr>
     135                        <tr>
     136                                <th scope="row"><?php esc_html_e( 'Port', 'kamboj-form-builder' ); ?></th>
     137                                <td>
     138                                        <input type="number" min="1" max="65535" name="<?php echo esc_attr( KMFB_Settings::OPTION_KEY ); ?>[smtp_port]" value="<?php echo esc_attr( (string) ( $kmfb_settings['smtp_port'] ?? 587 ) ); ?>" />
     139                                        <p class="description"><?php esc_html_e( '587 with TLS is the usual choice. 465 goes with SSL. 25 is often blocked by hosts.', 'kamboj-form-builder' ); ?></p>
     140                                </td>
     141                        </tr>
     142                        <tr>
     143                                <th scope="row"><?php esc_html_e( 'Encryption', 'kamboj-form-builder' ); ?></th>
     144                                <td>
     145                                        <?php $kmfb_smtp_enc = $kmfb_settings['smtp_encryption'] ?? 'tls'; ?>
     146                                        <select name="<?php echo esc_attr( KMFB_Settings::OPTION_KEY ); ?>[smtp_encryption]">
     147                                                <option value="tls" <?php selected( $kmfb_smtp_enc, 'tls' ); ?>><?php esc_html_e( 'TLS (STARTTLS)', 'kamboj-form-builder' ); ?></option>
     148                                                <option value="ssl" <?php selected( $kmfb_smtp_enc, 'ssl' ); ?>><?php esc_html_e( 'SSL', 'kamboj-form-builder' ); ?></option>
     149                                                <option value="none" <?php selected( $kmfb_smtp_enc, 'none' ); ?>><?php esc_html_e( 'None', 'kamboj-form-builder' ); ?></option>
     150                                        </select>
     151                                        <p class="description">
     152                                                <label>
     153                                                        <input type="checkbox" name="<?php echo esc_attr( KMFB_Settings::OPTION_KEY ); ?>[smtp_autotls]" value="1" <?php checked( ! isset( $kmfb_settings['smtp_autotls'] ) || ! empty( $kmfb_settings['smtp_autotls'] ) ); ?> />
     154                                                        <?php esc_html_e( 'Upgrade to TLS automatically when the server offers it', 'kamboj-form-builder' ); ?>
     155                                                </label>
     156                                        </p>
     157                                </td>
     158                        </tr>
     159                        <tr>
     160                                <th scope="row"><?php esc_html_e( 'Authentication', 'kamboj-form-builder' ); ?></th>
     161                                <td>
     162                                        <label>
     163                                                <input type="checkbox" name="<?php echo esc_attr( KMFB_Settings::OPTION_KEY ); ?>[smtp_auth]" value="1" <?php checked( ! isset( $kmfb_settings['smtp_auth'] ) || ! empty( $kmfb_settings['smtp_auth'] ) ); ?> />
     164                                                <?php esc_html_e( 'Sign in with a username and password', 'kamboj-form-builder' ); ?>
     165                                        </label>
     166                                </td>
     167                        </tr>
     168                        <tr>
     169                                <th scope="row"><?php esc_html_e( 'Username', 'kamboj-form-builder' ); ?></th>
     170                                <td>
     171                                        <input type="text" class="regular-text" name="<?php echo esc_attr( KMFB_Settings::OPTION_KEY ); ?>[smtp_username]" value="<?php echo esc_attr( $kmfb_settings['smtp_username'] ?? '' ); ?>" autocomplete="off" />
     172                                </td>
     173                        </tr>
     174                        <tr>
     175                                <th scope="row"><?php esc_html_e( 'Password', 'kamboj-form-builder' ); ?></th>
     176                                <td>
     177                                        <?php if ( $kmfb_smtp->password_is_constant() ) : ?>
     178                                                <p class="description"><strong><?php esc_html_e( 'Set in wp-config.php as KMFB_SMTP_PASSWORD, which takes precedence over anything saved here.', 'kamboj-form-builder' ); ?></strong></p>
     179                                        <?php else : ?>
     180                                                <input type="password" class="regular-text" name="<?php echo esc_attr( KMFB_Settings::OPTION_KEY ); ?>[smtp_password]" value="" autocomplete="new-password" placeholder="<?php echo esc_attr( ! empty( $kmfb_settings['smtp_password'] ) ? __( 'Leave blank to keep existing password', 'kamboj-form-builder' ) : '' ); ?>" />
     181                                                <p class="description">
     182                                                        <?php if ( ! empty( $kmfb_settings['smtp_password'] ) ) : ?>
     183                                                                <?php esc_html_e( 'A password is already saved. Leave this field blank to keep it.', 'kamboj-form-builder' ); ?><br />
     184                                                        <?php endif; ?>
     185                                                        <?php esc_html_e( 'Saved in the database. To keep it out of the database entirely, add this to wp-config.php instead:', 'kamboj-form-builder' ); ?>
     186                                                        <code>define( 'KMFB_SMTP_PASSWORD', 'your-password' );</code>
     187                                                </p>
     188                                        <?php endif; ?>
     189                                </td>
     190                        </tr>
     191                </table>
     192                <p class="description"><?php esc_html_e( 'Use the test email above to check these settings before relying on them.', 'kamboj-form-builder' ); ?></p>
    74193
    75194                <h2><?php esc_html_e( 'Validation messages', 'kamboj-form-builder' ); ?></h2>
    … …  
    92211                </table>
    93212
     213                <h2><?php esc_html_e( 'Captcha', 'kamboj-form-builder' ); ?></h2>
     214                <p class="description"><?php esc_html_e( 'Choose which service protects your forms, then fill in its keys below. Whichever you pick, switch it on per form under Form Fields → Form behavior.', 'kamboj-form-builder' ); ?></p>
     215                <table class="form-table" role="presentation">
     216                        <tr>
     217                                <th scope="row"><?php esc_html_e( 'Provider', 'kamboj-form-builder' ); ?></th>
     218                                <td>
     219                                        <?php $kmfb_provider = $kmfb_settings['captcha_provider'] ?? ''; ?>
     220                                        <select name="<?php echo esc_attr( KMFB_Settings::OPTION_KEY ); ?>[captcha_provider]">
     221                                                <option value="" <?php selected( $kmfb_provider, '' ); ?>><?php esc_html_e( 'None', 'kamboj-form-builder' ); ?></option>
     222                                                <option value="recaptcha" <?php selected( $kmfb_provider, 'recaptcha' ); ?>><?php esc_html_e( 'Google reCAPTCHA', 'kamboj-form-builder' ); ?></option>
     223                                                <option value="turnstile" <?php selected( $kmfb_provider, 'turnstile' ); ?>><?php esc_html_e( 'Cloudflare Turnstile', 'kamboj-form-builder' ); ?></option>
     224                                        </select>
     225                                        <?php if ( '' === $kmfb_provider && ! empty( $kmfb_settings['recaptcha_site_key'] ) && ! empty( $kmfb_settings['recaptcha_version'] ) ) : ?>
     226                                                <p class="description"><?php esc_html_e( 'reCAPTCHA keys are already saved, so reCAPTCHA stays in use until you choose otherwise here.', 'kamboj-form-builder' ); ?></p>
     227                                        <?php endif; ?>
     228                                </td>
     229                        </tr>
     230                </table>
     231
    94232                <h2><?php esc_html_e( 'Google reCAPTCHA', 'kamboj-form-builder' ); ?></h2>
    95233                <p class="description">
    96234                        <?php
    97                         printf(
    98                                 /* translators: %s: Google reCAPTCHA admin URL */
    99                                 esc_html__( 'Get your Site Key and Secret Key from %s. Choose v2 for the checkbox widget or v3 for invisible score-based protection.', 'kamboj-form-builder' ),
    100                                 '<a href="https://www.google.com/recaptcha/admin" target="_blank" rel="noopener noreferrer">Google reCAPTCHA Admin</a>'
     235                        $kmfb_recaptcha_admin_url = 'https://www.google.com/recaptcha/admin';
     236                        echo wp_kses(
     237                                sprintf(
     238                                        /* translators: %s: Google reCAPTCHA admin link */
     239                                        __( 'Get your Site Key and Secret Key from %s. Choose v2 for the checkbox widget or v3 for invisible score-based protection.', 'kamboj-form-builder' ),
     240                                        kmfb_settings_doc_link(
     241                                                $kmfb_recaptcha_admin_url,
     242                                                __( 'Google reCAPTCHA Admin', 'kamboj-form-builder' )
     243                                        )
     244                                ),
     245                                kmfb_settings_link_kses()
    101246                        );
    102247                        ?>
    … …  
    136281                        </tr>
    137282                </table>
    138                 <p class="description"><?php esc_html_e( 'After saving keys here, enable reCAPTCHA per form under Form Fields → Form behavior.', 'kamboj-form-builder' ); ?></p>
     283                <p class="description"><?php esc_html_e( 'Used when the provider above is set to Google reCAPTCHA.', 'kamboj-form-builder' ); ?></p>
     284
     285                <h2><?php esc_html_e( 'Cloudflare Turnstile', 'kamboj-form-builder' ); ?></h2>
     286                <p class="description">
     287                        <?php
     288                        // phpcs:ignore PluginCheck.CodeAnalysis.Offloading.OffloadedContent -- Admin help link to obtain optional Turnstile API keys; no assets loaded.
     289                        $kmfb_turnstile_dashboard_url = 'https://dash.cloudflare.com/?to=/:account/turnstile';
     290                        echo wp_kses(
     291                                sprintf(
     292                                        /* translators: %s: Cloudflare Turnstile dashboard link */
     293                                        __( 'Get your Site Key and Secret Key from %s. Turnstile is a privacy-friendly alternative to reCAPTCHA and is free to use.', 'kamboj-form-builder' ),
     294                                        kmfb_settings_doc_link(
     295                                                $kmfb_turnstile_dashboard_url,
     296                                                __( 'Cloudflare Turnstile', 'kamboj-form-builder' )
     297                                        )
     298                                ),
     299                                kmfb_settings_link_kses()
     300                        );
     301                        ?>
     302                </p>
     303                <table class="form-table" role="presentation">
     304                        <tr>
     305                                <th scope="row"><?php esc_html_e( 'Site key', 'kamboj-form-builder' ); ?></th>
     306                                <td>
     307                                        <input type="text" class="large-text" name="<?php echo esc_attr( KMFB_Settings::OPTION_KEY ); ?>[turnstile_site_key]" value="<?php echo esc_attr( $kmfb_settings['turnstile_site_key'] ?? '' ); ?>" autocomplete="off" />
     308                                </td>
     309                        </tr>
     310                        <tr>
     311                                <th scope="row"><?php esc_html_e( 'Secret key', 'kamboj-form-builder' ); ?></th>
     312                                <td>
     313                                        <input type="password" class="large-text" name="<?php echo esc_attr( KMFB_Settings::OPTION_KEY ); ?>[turnstile_secret_key]" value="" autocomplete="new-password" placeholder="<?php echo esc_attr( ! empty( $kmfb_settings['turnstile_secret_key'] ) ? __( 'Leave blank to keep existing key', 'kamboj-form-builder' ) : '' ); ?>" />
     314                                        <?php if ( ! empty( $kmfb_settings['turnstile_secret_key'] ) ) : ?>
     315                                                <p class="description"><?php esc_html_e( 'A secret key is already saved. Leave this field blank to keep it.', 'kamboj-form-builder' ); ?></p>
     316                                        <?php endif; ?>
     317                                </td>
     318                        </tr>
     319                        <tr>
     320                                <th scope="row"><?php esc_html_e( 'Widget theme', 'kamboj-form-builder' ); ?></th>
     321                                <td>
     322                                        <?php $kmfb_ts_theme = $kmfb_settings['turnstile_theme'] ?? 'auto'; ?>
     323                                        <select name="<?php echo esc_attr( KMFB_Settings::OPTION_KEY ); ?>[turnstile_theme]">
     324                                                <option value="auto" <?php selected( $kmfb_ts_theme, 'auto' ); ?>><?php esc_html_e( 'Auto', 'kamboj-form-builder' ); ?></option>
     325                                                <option value="light" <?php selected( $kmfb_ts_theme, 'light' ); ?>><?php esc_html_e( 'Light', 'kamboj-form-builder' ); ?></option>
     326                                                <option value="dark" <?php selected( $kmfb_ts_theme, 'dark' ); ?>><?php esc_html_e( 'Dark', 'kamboj-form-builder' ); ?></option>
     327                                        </select>
     328                                </td>
     329                        </tr>
     330                </table>
     331                <p class="description"><?php esc_html_e( 'Used when the provider above is set to Cloudflare Turnstile.', 'kamboj-form-builder' ); ?></p>
    139332
    140333                <h2><?php esc_html_e( 'Spam protection & uploads', 'kamboj-form-builder' ); ?></h2>
  • kamboj-form-builder/trunk/kamboj-form-builder.php

    r3702554 r3779408  
    33 * Plugin Name:       Kamboj Form Builder – Drag & Drop Contact Forms
    44 * Plugin URI:        https://wordpress.org/plugins/kamboj-form-builder/
    5  * Description:       Create professional contact forms, feedback forms, and custom WordPress forms with a drag & drop builder, AJAX, reCAPTCHA, webhooks, and CSV export.
    6  * Version:           1.7.7
     5 * Description:       Create professional contact forms, feedback forms, and custom WordPress forms with a drag & drop builder, AJAX, reCAPTCHA or Cloudflare Turnstile, SMTP, webhooks, and CSV export.
     6 * Version:           1.10.7
    77 * Requires at least: 6.0
    88 * Tested up to:      7.1
    … …  
    1919defined( 'ABSPATH' ) || exit;
    2020
    21 define( 'KMFB_VERSION', '1.7.7' );
     21define( 'KMFB_VERSION', '1.10.7' );
    2222define( 'KMFB_PLUGIN_FILE', __FILE__ );
    2323define( 'KMFB_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
    … …  
    3737kmfb_plugin();
    3838
    39 register_activation_hook( __FILE__, array( 'KMFB_Installer', 'activate' ) );
     39// Network aware: activating across a multisite network has to install on
     40// every site, not only the one the activation was triggered from.
     41register_activation_hook( __FILE__, array( 'KMFB_Multisite', 'activate' ) );
    4042register_deactivation_hook( __FILE__, array( 'KMFB_Installer', 'deactivate' ) );
  • kamboj-form-builder/trunk/public/js/form.js

    r3699084 r3779408  
    22  function closestForm(el) {
    33    return el.closest('.kmfb-form');
     4  }
     5
     6  function formI18n(form) {
     7    const base = typeof kmfbForm !== 'undefined' && kmfbForm.i18n ? kmfbForm.i18n : {};
     8    const raw = form?.dataset?.messages;
     9    if (!raw) {
     10      return base;
     11    }
     12    try {
     13      return Object.assign({}, base, JSON.parse(raw));
     14    } catch (error) {
     15      return base;
     16    }
     17  }
     18
     19  function formLang(form) {
     20    if (form?.dataset?.lang) {
     21      return form.dataset.lang;
     22    }
     23    return typeof kmfbForm !== 'undefined' ? kmfbForm.lang || '' : '';
    424  }
    525
    … …  
    144164
    145165  async function prepareRecaptcha(form) {
     166    const i18n = formI18n(form);
     167    const provider = form.dataset.captchaProvider;
     168
     169    // Cloudflare Turnstile renders itself and writes its own token into the
     170    // form, so there is nothing to execute -- only a check that the visitor
     171    // has actually completed it before the submission goes out.
     172    if (provider === 'turnstile') {
     173      if (typeof turnstile === 'undefined') {
     174        showMessage(form, i18n.turnstileFailed || i18n.recaptchaFailed, 'error');
     175        return false;
     176      }
     177      const field = form.querySelector('[name="cf-turnstile-response"]');
     178      if (!field || !field.value) {
     179        showMessage(form, i18n.turnstileRequired || i18n.recaptchaRequired, 'error');
     180        return false;
     181      }
     182      return true;
     183    }
     184
    146185    const version = form.dataset.recaptchaVersion;
    147186    if (!version) {
    … …  
    150189
    151190    if (typeof grecaptcha === 'undefined') {
    152       showMessage(form, kmfbForm.i18n.recaptchaFailed, 'error');
     191      showMessage(form, i18n.recaptchaFailed, 'error');
    153192      return false;
    154193    }
    … …  
    162201      }
    163202      if (!response) {
    164         showMessage(form, kmfbForm.i18n.recaptchaRequired, 'error');
     203        showMessage(form, i18n.recaptchaRequired, 'error');
    165204        return false;
    166205      }
    … …  
    182221        return true;
    183222      } catch (error) {
    184         showMessage(form, kmfbForm.i18n.recaptchaFailed, 'error');
     223        showMessage(form, i18n.recaptchaFailed, 'error');
    185224        return false;
    186225      }
    … …  
    191230
    192231  function resetRecaptcha(form) {
     232    if (form.dataset.captchaProvider === 'turnstile') {
     233      if (typeof turnstile !== 'undefined') {
     234        try {
     235          turnstile.reset();
     236        } catch (error) {
     237          /* ignore */
     238        }
     239      }
     240      return;
     241    }
     242
    193243    if (form.dataset.recaptchaVersion === 'v2' && typeof grecaptcha !== 'undefined') {
    194244      try {
    … …  
    256306    event.preventDefault();
    257307
     308    const i18n = formI18n(form);
     309
    258310    clearFieldErrors(form);
    259     showMessage(form, kmfbForm.i18n.sending, 'info');
     311    showMessage(form, i18n.sending, 'info');
    260312
    261313    const submitBtn = form.querySelector('.kmfb-submit');
    … …  
    271323
    272324    const body = new FormData(form);
     325    const lang = formLang(form);
     326    if (lang) {
     327      body.append('lang', lang);
     328    }
    273329
    274330    try {
    … …  
    287343          showMessage(form, '', 'error');
    288344        } else {
    289           showMessage(form, json.data?.message || kmfbForm.i18n.error, 'error');
     345          showMessage(form, json.data?.message || i18n.error, 'error');
    290346        }
    291347
    … …  
    299355      }
    300356
    301       showMessage(form, json.data.message || kmfbForm.i18n.success, 'success');
     357      showMessage(form, json.data.message || i18n.success, 'success');
    302358      form.reset();
    303359      resetRecaptcha(form);
    … …  
    308364      }
    309365    } catch (error) {
    310       showMessage(form, kmfbForm.i18n.error, 'error');
     366      showMessage(form, i18n.error, 'error');
    311367      resetRecaptcha(form);
    312368    } finally {
  • kamboj-form-builder/trunk/readme.txt

    r3702554 r3779408  
    11=== Kamboj Form Builder – Drag & Drop Contact Forms ===
    22Contributors: mohitkamboj55
    3 Tags: contact form, form builder, contact forms, drag and drop, custom forms
     3Tags: contact form, form builder, smtp, turnstile, multilingual
    44Requires at least: 6.0
    55Tested up to: 7.1
    66Requires PHP: 7.4
    7 Stable tag: 1.7.7
     7Stable tag: 1.10.7
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    2626✔ Built-in Form Entries 
    2727✔ CSV Export 
    28 ✔ Google reCAPTCHA
     28✔ Google reCAPTCHA or Cloudflare Turnstile
    2929✔ Webhooks 
     30✔ SMTP 
    3031✔ Conditional Logic 
    3132✔ Developer Friendly 
    … …  
    120121
    121122== Changelog ==
     123
     124= 1.10.7 =
     125* Plugin Check: avoid captcha verification host literals in PHP source.
     126
     127= 1.10.6 =
     128* Plugin Check: captcha providers load via local loader scripts; remove .gitignore from plugin package.
     129
     130= 1.10.5 =
     131* Plugin Check: load optional captcha widgets via script_loader_tag instead of remote wp_enqueue_script URLs.
     132
     133= 1.10.4 =
     134* Plugin Check: document optional captcha CDN/API usage and fix settings help links.
     135* WPML hook calls moved to a dedicated bridge class for cleaner Plugin Check results.
     136
     137= 1.10.3 =
     138* Multilingual: per-form Validation tab messages and field custom error messages
     139  register in WPML / Polylang String Translation and translate on display.
     140* Polylang support alongside WPML (language detection, AJAX language switch,
     141  string registration).
     142* Each form passes its own frontend validation strings (per-form overrides
     143  and translated success message) for correct AJAX UX.
     144* wpml-config and builder validation list include Turnstile message keys.
     145* Duplicate form uses the original stored title, not a translated title.
     146
     147= 1.10.2 =
     148* The confirmation email sent back to whoever filled the form is now
     149  translatable, subject and body both. It goes to the visitor, so it follows
     150  the language they filled the form in.
     151* The notification that goes to the site owner is deliberately left in one
     152  language, since the same person reads it whichever language the enquiry
     153  came from.
     154* Fixed: a form saved in the admin registered only part of its wording for
     155  translation, so anything not in that part was never offered. The whole form
     156  is registered now.
     157* Merge tags such as {{name}} and {{site_name}} are kept intact by a
     158  translation.
     159
     160= 1.10.1 =
     161* Fixed: on a multilingual site the reply to a submission came back in the
     162  site's default language rather than the one the visitor was reading. A form
     163  posts to admin-ajax.php, whose URL carries no language segment, so there was
     164  nothing for the translation plugin to detect -- an Arabic visitor filled in
     165  an Arabic form and got an English success notice, English validation errors
     166  and an English notification email. The language now travels with the
     167  submission, and only a language the site actually serves is accepted.
     168* Works on sites storing regional language codes ("ar-ae" rather than "ar").
     169
     170= 1.10.0 =
     171* New: SMTP. WordPress hands mail to the server's own PHP mail by default,
     172  which many hosts send unauthenticated -- a common reason notifications land
     173  in spam or never arrive. Settings -> SMTP points the plugin at a real
     174  mailbox or sending service instead.
     175* The SMTP settings apply to this plugin's own mail by default, so a site
     176  already running a dedicated SMTP plugin is untouched. Switch the scope to
     177  "All email from this site" to have it handle everything.
     178* The password can be set as KMFB_SMTP_PASSWORD in wp-config.php, keeping it
     179  out of the database. The settings screen says when that is in effect.
     180* Configured through the standard phpmailer_init hook rather than by
     181  replacing wp_mail(), so it stacks properly with other plugins.
     182* New: an Arabic translation of the plugin's own wording, so the messages a
     183  visitor sees -- validation errors, the sending and thank-you notices, the
     184  dropdown prompt -- are translated out of the box.
     185
     186= 1.9.0 =
     187* New: Cloudflare Turnstile, alongside Google reCAPTCHA. Pick the provider
     188  under Settings -> Captcha and fill in its keys; the per-form switch under
     189  Form behavior works the same for either one.
     190* Turnstile keys are stored separately from reCAPTCHA keys, so switching
     191  between the two -- or back again -- never overwrites the other's keys.
     192* A site already using reCAPTCHA keeps using it after this update with no
     193  change and nothing to set.
     194* Both providers now share one implementation, so a form is verified the
     195  same way whichever is in use.
     196
     197= 1.8.0 =
     198* Multilingual: a form's own wording -- its labels, placeholders, dropdown
     199  choices, button and success message -- is now handed to WPML String
     200  Translation, so one form serves every language. The same shortcode works on
     201  a translated page; there is no second copy of the form to keep in step.
     202* A choice still submits the value it was written with, whatever language it
     203  is shown in, so answers to the same question stay comparable across
     204  languages and existing entries keep their meaning.
     205* Adds wpml-config.xml, so WPML knows the form post type is handled by string
     206  translation and leaves its stored field definitions alone.
     207* Multisite: network activation now installs on every site rather than only
     208  the one it was triggered from, a site created later installs on creation,
     209  and any site whose table is missing installs itself on next load. Before
     210  this, forms on every site but the first failed to record submissions.
     211* Multisite: uninstall now cleans up across the network, checking each site's
     212  own "delete data on uninstall" setting rather than one site's.
     213* New filter `kmfb_form_package` and action `kmfb_form_saved` for extensions.
    122214
    123215= 1.7.7 =
  • kamboj-form-builder/trunk/templates/form-wrapper.php

    r3699084 r3779408  
    1212$kmfb_renderer         = kmfb_plugin()->renderer;
    1313$kmfb_settings         = $kmfb_form['settings'];
    14 $kmfb_recaptcha        = kmfb_plugin()->recaptcha;
     14$kmfb_recaptcha        = kmfb_plugin()->captcha();
    1515$kmfb_recaptcha_active = $kmfb_recaptcha->is_active_for_form( $kmfb_form );
    1616$kmfb_recaptcha_config = $kmfb_recaptcha->get_config();
    … …  
    2424                enctype="multipart/form-data"
    2525                novalidate
     26                data-lang="<?php echo esc_attr( KMFB_I18n::current_language_code() ); ?>"
     27                data-messages="<?php echo esc_attr( wp_json_encode( KMFB_I18n::client_i18n( $kmfb_form ) ) ); ?>"
    2628                <?php if ( $kmfb_recaptcha_active ) : ?>
     29                        data-captcha-provider="<?php echo esc_attr( $kmfb_recaptcha_config['provider'] ); ?>"
    2730                        data-recaptcha-version="<?php echo esc_attr( $kmfb_recaptcha_config['version'] ); ?>"
    2831                        data-recaptcha-site-key="<?php echo esc_attr( $kmfb_recaptcha_config['site_key'] ); ?>"
    … …  
    5154                </div>
    5255
    53                 <?php if ( $kmfb_recaptcha_active && 'v2' === $kmfb_recaptcha_config['version'] ) : ?>
     56                <?php
     57                $kmfb_captcha_widget = $kmfb_recaptcha_active ? $kmfb_recaptcha->widget_html() : '';
     58                if ( '' !== $kmfb_captcha_widget ) :
     59                        ?>
    5460                        <div class="kmfb-recaptcha">
    55                                 <div class="g-recaptcha" data-sitekey="<?php echo esc_attr( $kmfb_recaptcha_config['site_key'] ); ?>"></div>
     61                                <?php echo $kmfb_captcha_widget; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- built and escaped by the provider ?>
    5662                        </div>
    5763                <?php endif; ?>
  • kamboj-form-builder/trunk/uninstall.php

    r3594208 r3779408  
    88defined( 'WP_UNINSTALL_PLUGIN' ) || exit;
    99
    10 $kmfb_settings = get_option( 'kmfb_settings', array() );
    11 if ( ! is_array( $kmfb_settings ) || empty( $kmfb_settings['delete_data_on_uninstall'] ) ) {
    12         return;
    13 }
     10require_once __DIR__ . '/includes/class-kmfb-installer.php';
     11require_once __DIR__ . '/includes/class-kmfb-multisite.php';
    1412
    15 require_once __DIR__ . '/includes/class-kmfb-installer.php';
     13/**
     14 * Remove this site's forms, submissions and options.
     15 *
     16 * The opt-in setting is read inside each site, so one site choosing to delete
     17 * its data on uninstall never removes another site's enquiries.
     18 */
     19$kmfb_delete_site_data = static function () {
     20        $kmfb_settings = get_option( 'kmfb_settings', array() );
    1621
    17 $kmfb_form_ids = get_posts(
    18         array(
    19                 'post_type'      => array( 'kmfb_form', 'mfm_form' ),
    20                 'post_status'    => 'any',
    21                 'posts_per_page' => -1,
    22                 'fields'         => 'ids',
    23         )
    24 );
     22        if ( ! is_array( $kmfb_settings ) || empty( $kmfb_settings['delete_data_on_uninstall'] ) ) {
     23                return;
     24        }
    2525
    26 foreach ( $kmfb_form_ids as $kmfb_form_id ) {
    27         wp_delete_post( (int) $kmfb_form_id, true );
    28 }
     26        $kmfb_form_ids = get_posts(
     27                array(
     28                        'post_type'      => array( 'kmfb_form', 'mfm_form' ),
     29                        'post_status'    => 'any',
     30                        'posts_per_page' => -1,
     31                        'fields'         => 'ids',
     32                )
     33        );
    2934
    30 KMFB_Installer::drop_tables();
     35        foreach ( $kmfb_form_ids as $kmfb_form_id ) {
     36                wp_delete_post( (int) $kmfb_form_id, true );
     37        }
    3138
    32 delete_option( 'kmfb_settings' );
    33 delete_option( 'kmfb_mail_log' );
    34 delete_option( 'kmfb_db_version' );
    35 delete_option( 'kmfb_notification_fix_v1' );
     39        KMFB_Installer::drop_tables();
     40
     41        delete_option( 'kmfb_settings' );
     42        delete_option( 'kmfb_mail_log' );
     43        delete_option( 'kmfb_db_version' );
     44        delete_option( 'kmfb_notification_fix_v1' );
     45        delete_option( 'kmfb_legacy_migrate_v1' );
     46};
     47
     48KMFB_Multisite::uninstall_network( $kmfb_delete_site_data );
Note: See TracChangeset for help on using the changeset viewer.