Plugin Directory

Changeset 3763733


Ignore:
Timestamp:
10/24/2026 09:00:21 PM (3 hours ago)
Author:
2ranq
Message:

2ranq Forms 1.2.0. A send from a cached page no longer disappears: the nonce is only asked of logged-in visitors, every rejected send shows a message and gives the typed values back, and Cloudflare Turnstile returns as the third spam check.

Location:
2ranq-forms
Files:
39 edited
10 copied

Legend:

Unmodified
Added
Removed
  • 2ranq-forms/tags/1.2.0/2ranq-forms.php

    r3688493 r3763733  
    44 * Plugin URI:        https://2ranq.com/plugins/2ranq-forms/
    55 * Description:       Form builder with conditional logic, multi-step forms and file uploads. Every feature free, nothing locked.
    6  * Version:           1.1.0
     6 * Version:           1.2.0
    77 * Requires at least: 6.6
    88 * Requires PHP:      7.4
    … …  
    3333 */
    3434
    35 define( 'RANQ_FORMS_VERSION', '1.1.0' );
     35define( 'RANQ_FORMS_VERSION', '1.2.0' );
    3636define( 'RANQ_FORMS_FILE', __FILE__ );
    3737define( 'RANQ_FORMS_DIR', plugin_dir_path( __FILE__ ) );
  • 2ranq-forms/tags/1.2.0/includes/class-ranq-forms-spam.php

    r3681524 r3763733  
    33 * Spam protection, with the keys of outside services.
    44 *
    5  * Google reCAPTCHA (v2 checkbox, v2 invisible, v3) and hCaptcha, all unlocked.
    6  * The owner enters the keys once, and every form can use them.
    7  *
    8  * Cloudflare Turnstile WORKED and was taken out of the first release, because
    9  * the official Plugin Check flags every `cloudflare.com` and rejects the
    10  * submission by machine. The whole removed code and the procedure for putting it
    11  * back are in `13-TURNSTILE-VRACANJE.md`.
     5 * Google reCAPTCHA (v2 checkbox, v2 invisible, v3), hCaptcha and Cloudflare
     6 * Turnstile, all unlocked. The owner enters the keys once, and every form can
     7 * use them.
     8 *
     9 * Turnstile sat out the first release: the Plugin Check "offloading" sniff
     10 * flags every `cloudflare.com` address because Cloudflare also serves other
     11 * people's files, and an error of that level rejects a NEW submission by
     12 * machine. Turnstile is not file offloading, it is a visitor check, the same
     13 * as the two services above it that the sniff does not flag. It came back in
     14 * 1.2.0, with the sniff told so on each of the two addresses.
    1215 *
    1316 * The check runs ON THE SERVER. Without that the protection is worth nothing,
    … …  
    8184                                'script' => 'https://js.hcaptcha.com/1/api.js',
    8285                        ),
     86                        'turnstile' => array(
     87                                'name'   => __( 'Cloudflare Turnstile', '2ranq-forms' ),
     88                                'note'   => __( 'Free, quiet, and usually invisible to the visitor. The easiest of the three.', '2ranq-forms' ),
     89                                // The dashboard address under the button the owner presses to go and
     90                                // make their keys, the same as the two services above have. Nothing
     91                                // is loaded from it, so it is not offloading either.
     92                                'link'   => 'https://dash.cloudflare.com/?to=/:account/turnstile', // phpcs:ignore PluginCheck.CodeAnalysis.Offloading.OffloadedContent
     93                                'link_text' => __( 'Open Turnstile in Cloudflare', '2ranq-forms' ),
     94                                'steps'  => array(
     95                                        __( 'Make a free Cloudflare account. Your site does not have to use Cloudflare for anything else.', '2ranq-forms' ),
     96                                        __( 'Open Turnstile, press Add widget, type your domain, and leave the mode on Managed.', '2ranq-forms' ),
     97                                        __( 'Copy the Site Key and the Secret Key into the two boxes below.', '2ranq-forms' ),
     98                                ),
     99                                // The two addresses below are the visitor check itself, not content
     100                                // offloading: nothing of the site is served from them, they only
     101                                // answer whether the visitor passed. The sniff cannot tell the two
     102                                // apart because both live on cloudflare.com, so it is told here.
     103                                'verify' => 'https://challenges.cloudflare.com/turnstile/v0/siteverify', // phpcs:ignore PluginCheck.CodeAnalysis.Offloading.OffloadedContent
     104                                'script' => 'https://challenges.cloudflare.com/turnstile/v0/api.js', // phpcs:ignore PluginCheck.CodeAnalysis.Offloading.OffloadedContent
     105                        ),
    83106                );
    84107        }
    … …  
    221244                }
    222245
    223                 $klasa = 'hcaptcha' === $s['provider'] ? 'h-captcha' : 'g-recaptcha';
     246                $klasa = 'hcaptcha' === $s['provider'] ? 'h-captcha' : ( 'turnstile' === $s['provider'] ? 'cf-turnstile' : 'g-recaptcha' );
    224247                $extra = '';
    225248
    … …  
    304327                if ( '' === $odgovor ) {
    305328                        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- the same.
     329                        $odgovor = isset( $_POST['cf-turnstile-response'] ) ? sanitize_text_field( wp_unslash( $_POST['cf-turnstile-response'] ) ) : '';
     330                }
     331
     332                if ( '' === $odgovor ) {
     333                        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- the same.
    306334                        $odgovor = isset( $_POST['ranq_spam_token'] ) ? sanitize_text_field( wp_unslash( $_POST['ranq_spam_token'] ) ) : '';
    307335                }
  • 2ranq-forms/tags/1.2.0/includes/class-ranq-forms-submission.php

    r3681524 r3763733  
    4444                }
    4545
    46                 if ( ! isset( $_POST['ranq_forms_id'], $_POST['ranq_forms_nonce'] ) ) {
     46                if ( ! isset( $_POST['ranq_forms_id'] ) ) {
    4747                        return;
    4848                }
    … …  
    5151
    5252                if ( ! $form_id ) {
    53                         return;
    54                 }
    55 
    56                 $nonce = sanitize_text_field( wp_unslash( $_POST['ranq_forms_nonce'] ) );
    57 
    58                 if ( ! wp_verify_nonce( $nonce, 'ranq_forms_submit_' . $form_id ) ) {
    59                         return;
    60                 }
    61 
    62                 // The bot trap. The field sits in the form, moved off the screen, and a person
    63                 // neither sees it nor can fill it in with the keyboard. A robot that fills in
    64                 // everything it finds fills that in too. It is rejected quietly, with no
    65                 // message, because a message tells the robot where the mistake is.
    66                 if ( ! empty( $_POST['ranq_hp'] ) ) {
    6753                        return;
    6854                }
    … …  
    7258                // swallow the error and the visitor would stare at a blank form with nothing.
    7359                self::$submitted = $form_id;
     60
     61                // The nonce is only asked of a logged-in visitor, because only there it does
     62                // its real job: it stops another site from riding that visitor's session. A
     63                // guest has no session to ride, so for a guest the nonce protected nothing a
     64                // robot could not get around by fetching a fresh page, and it did real harm:
     65                // a cache plugin happily serves the same page for a week while the nonce in
     66                // it dies after a day, so a visitor sending from a cached page was thrown
     67                // away without a word. Contact Form 7 and Gravity Forms treat a public form
     68                // the same way. What actually guards a public form are the bot trap and the
     69                // rate limit below, which do not age with the page.
     70                if ( is_user_logged_in() ) {
     71
     72                        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- the nonce is only compared, never stored or printed.
     73                        $nonce = isset( $_POST['ranq_forms_nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['ranq_forms_nonce'] ) ) : '';
     74
     75                        if ( ! wp_verify_nonce( $nonce, 'ranq_forms_submit_' . $form_id ) ) {
     76
     77                                // Never silently. The page the visitor sent from had simply been open,
     78                                // or cached, for longer than the nonce lives. They get told, and the
     79                                // form below gives them their typed values back, so nothing is lost.
     80                                self::$errors = array( '_form' => __( 'This page had been open for too long. Please send the form again.', '2ranq-forms' ) );
     81
     82                                return;
     83                        }
     84                }
     85
     86                // The bot trap. The field sits in the form, moved off the screen, and a person
     87                // neither sees it nor can fill it in with the keyboard. A robot that fills in
     88                // everything it finds fills that in too. The message is deliberately vague, it
     89                // does not say which check failed, so a robot learns nothing from it. But it
     90                // is no longer silent, because a browser extension that autofills every field
     91                // it finds can trip the trap for a real person. That person gets their text
     92                // back, the trap field renders empty again, and the second send goes through.
     93                if ( ! empty( $_POST['ranq_hp'] ) ) {
     94                        self::$errors = array( '_form' => __( 'The form could not be sent. Please try again.', '2ranq-forms' ) );
     95
     96                        return;
     97                }
    7498
    7599                // The rate limit. Without this one person can fill the database and, through
    … …  
    286310
    287311                return self::$sent && self::$submitted === (int) $form_id;
     312        }
     313
     314        /**
     315         * What the visitor sent for one field, raw, or null.
     316         *
     317         * It only answers while THIS form's send has just failed, so the fields can
     318         * give the visitor their typed values back instead of a blank form. Any other
     319         * time, for any other form on the page, and after a send that went through,
     320         * it stays quiet and the field falls back to its default. The value comes
     321         * back raw on purpose: only the field's own class knows how to clean it, so
     322         * cleaning happens there, never here.
     323         *
     324         * @param int    $form_id The form id.
     325         * @param string $key     The field key.
     326         *
     327         * @return mixed The raw sent value, or null when there is nothing to give back.
     328         */
     329        public static function posted_raw( $form_id, $key ) {
     330
     331                if ( self::$sent || self::$submitted !== (int) $form_id ) {
     332                        return null;
     333                }
     334
     335                // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- read only to hand the visitor's text back to the field, which cleans it with its own sanitize() before printing.
     336                $raw = isset( $_POST['ranq_fields'] ) && is_array( $_POST['ranq_fields'] ) ? wp_unslash( $_POST['ranq_fields'] ) : array();
     337
     338                return isset( $raw[ $key ] ) ? $raw[ $key ] : null;
    288339        }
    289340
  • 2ranq-forms/tags/1.2.0/includes/fields/abstract-ranq-forms-field-choices.php

    r3688493 r3763733  
    107107
    108108        /**
     109         * Whether one choice renders as picked.
     110         *
     111         * While the form is fresh the owner's defaults decide. The moment a send has
     112         * failed, what the visitor picked wins, even when they picked nothing, because
     113         * putting the defaults back would quietly change their answer under them.
     114         *
     115         * @param array $choice One choice, as get_choices() returns it.
     116         * @param mixed $sticky What sticky_value() gave for the whole field.
     117         *
     118         * @return bool
     119         */
     120        protected function choice_on( $choice, $sticky ) {
     121
     122                if ( null === $sticky ) {
     123                        return ! empty( $choice['default'] );
     124                }
     125
     126                $picked = array_map( 'strval', array_filter( is_array( $sticky ) ? $sticky : array( $sticky ), 'is_scalar' ) );
     127
     128                return in_array( (string) $choice['value'], $picked, true );
     129        }
     130
     131        /**
    109132         * The value prepared for display, as the label the visitor actually saw.
    110133         *
  • 2ranq-forms/tags/1.2.0/includes/fields/abstract-ranq-forms-field-input.php

    r3681524 r3763733  
    7777                $mode        = $this->input_mode();
    7878                $auto        = $this->autocomplete();
     79
     80                // After a failed send the visitor's own text comes back, otherwise the
     81                // owner's default. The two never mix: a failed send with an emptied field
     82                // stays empty, because the default would change the answer under them.
     83                $sticky = $this->sticky_value( $field, $form_id );
     84                $value  = is_string( $sticky )
     85                        ? $sticky
     86                        : ( empty( $field['default_value'] ) ? '' : (string) $field['default_value'] );
    7987                ?>
    8088                <div class="ranq-field ranq-field--<?php echo esc_attr( $this->get_type() ); ?><?php echo esc_attr( $this->extra_classes( $field ) ); ?>"<?php echo $this->wrapper_data( $field ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- already escaped in wrapper_data().?>>
    … …  
    94102                                        placeholder="<?php echo esc_attr( $field['placeholder'] ); ?>"
    95103                                <?php endif; ?>
    96                                 <?php if ( ! empty( $field['default_value'] ) ) : ?>
    97                                         value="<?php echo esc_attr( $field['default_value'] ); ?>"
     104                                <?php if ( '' !== $value ) : ?>
     105                                        value="<?php echo esc_attr( $value ); ?>"
    98106                                <?php endif; ?>
    99107                                <?php if ( $describedby ) : ?>
  • 2ranq-forms/tags/1.2.0/includes/fields/abstract-ranq-forms-field.php

    r3681524 r3763733  
    329329
    330330        /**
     331         * The value the field shows when the form comes back after a failed send.
     332         *
     333         * A send can fail long after the visitor wrote their text: a required field
     334         * was missed, the spam check said no, the page had been cached for a week. A
     335         * blank form at that moment throws the whole message away, and nobody types
     336         * it twice. So every field that can, gives the text back. The value is
     337         * cleaned by the field's own sanitize(), because what came in is as raw as
     338         * any other sent value. Null means there is nothing to give back, and the
     339         * field falls back to its default. A password and a file stay out of this on
     340         * purpose: a password is never printed back into a page, and a browser does
     341         * not allow a file input to be prefilled at all.
     342         *
     343         * @param array $field   The field settings.
     344         * @param int   $form_id The form id.
     345         *
     346         * @return mixed The cleaned sent value, or null when there is none.
     347         */
     348        protected function sticky_value( $field, $form_id ) {
     349
     350                $raw = Ranq_Forms_Submission::posted_raw( $form_id, $this->field_key( $field ) );
     351
     352                if ( null === $raw ) {
     353                        return null;
     354                }
     355
     356                return $this->sanitize( $raw, $field );
     357        }
     358
     359        /**
    331360         * The element id in the HTML, has to be unique per form.
    332361         *
  • 2ranq-forms/tags/1.2.0/includes/fields/class-ranq-forms-field-address.php

    r3681524 r3763733  
    103103                $describedby = $this->describedby( $field, $form_id );
    104104                $base        = $this->input_id( $field, $form_id );
     105
     106                // After a failed send the visitor's address comes back instead of blank boxes.
     107                $sticky = $this->sticky_value( $field, $form_id );
    105108                ?>
    106109                <div class="ranq-field ranq-field--address<?php echo esc_attr( $this->extra_classes( $field ) ); ?>"<?php echo $this->wrapper_data( $field ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- already escaped in wrapper_data().?>>
    … …  
    124127                                                                class="ranq-field__input"
    125128                                                                autocomplete="<?php echo esc_attr( $part[1] ); ?>"
     129                                                                <?php if ( is_array( $sticky ) && isset( $sticky[ $key ] ) && '' !== $sticky[ $key ] ) : ?>
     130                                                                        value="<?php echo esc_attr( $sticky[ $key ] ); ?>"
     131                                                                <?php endif; ?>
    126132                                                        />
    127133                                                        <label class="ranq-field__sublabel" for="<?php echo esc_attr( $base . '-' . $key ); ?>">
  • 2ranq-forms/tags/1.2.0/includes/fields/class-ranq-forms-field-checkbox.php

    r3681524 r3763733  
    8686                $choices     = $this->get_choices( $field );
    8787                $base_id     = $this->input_id( $field, $form_id );
     88
     89                // After a failed send the visitor's ticks come back instead of the defaults.
     90                $sticky = $this->sticky_value( $field, $form_id );
    8891                ?>
    8992                <div class="ranq-field ranq-field--checkbox<?php echo esc_attr( $this->extra_classes( $field ) ); ?>"<?php echo $this->wrapper_data( $field ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- already escaped in wrapper_data().?>>
    … …  
    112115                                                        name="<?php echo esc_attr( $this->input_name( $field ) ); ?>"
    113116                                                        value="<?php echo esc_attr( $choice['value'] ); ?>"
    114                                                         <?php checked( true, $choice['default'] ); ?>
     117                                                        <?php checked( true, $this->choice_on( $choice, $sticky ) ); ?>
    115118                                                />
    116119                                                <label for="<?php echo esc_attr( $choice_id ); ?>">
  • 2ranq-forms/tags/1.2.0/includes/fields/class-ranq-forms-field-email.php

    r3681524 r3763733  
    6666
    6767        /**
     68         * What comes back into the box after a failed send.
     69         *
     70         * sanitize_email() throws away anything that is not a whole address, and the
     71         * most common reason this very field fails is a half-typed address. Giving
     72         * back an empty box would hide the typo the visitor needs to see to fix it.
     73         * So what comes back is cleaned as plain text instead, escaped on printing
     74         * like every other value, while the strict cleaning above still guards
     75         * everything that gets stored or mailed.
     76         *
     77         * @param array $field   The field settings.
     78         * @param int   $form_id The form id.
     79         *
     80         * @return mixed The sent text, or null when there is none.
     81         */
     82        protected function sticky_value( $field, $form_id ) {
     83
     84                $raw = Ranq_Forms_Submission::posted_raw( $form_id, $this->field_key( $field ) );
     85
     86                if ( null === $raw ) {
     87                        return null;
     88                }
     89
     90                return sanitize_text_field( is_scalar( $raw ) ? (string) $raw : '' );
     91        }
     92
     93        /**
    6894         * Prints the field.
    6995         *
    … …  
    76102
    77103                $describedby = $this->describedby( $field, $form_id );
     104
     105                // After a failed send the visitor's address comes back instead of a blank box.
     106                $sticky = $this->sticky_value( $field, $form_id );
    78107                ?>
    79108                <div class="ranq-field ranq-field--email<?php echo esc_attr( $this->extra_classes( $field ) ); ?>"<?php echo $this->wrapper_data( $field ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- already escaped in wrapper_data().?>>
    … …  
    86115                                autocomplete="email"
    87116                                inputmode="email"
     117                                <?php if ( is_string( $sticky ) && '' !== $sticky ) : ?>
     118                                        value="<?php echo esc_attr( $sticky ); ?>"
     119                                <?php endif; ?>
    88120                                <?php if ( ! empty( $field['placeholder'] ) ) : ?>
    89121                                        placeholder="<?php echo esc_attr( $field['placeholder'] ); ?>"
  • 2ranq-forms/tags/1.2.0/includes/fields/class-ranq-forms-field-imagechoice.php

    r3681524 r3763733  
    155155                $base_id     = $this->input_id( $field, $form_id );
    156156                $layout      = isset( $field['picture_layout'] ) && 'list' === $field['picture_layout'] ? 'list' : 'grid';
     157
     158                // After a failed send the visitor's pick comes back instead of the default.
     159                $sticky = $this->sticky_value( $field, $form_id );
    157160                ?>
    158161                <div class="ranq-field ranq-field--imagechoice ranq-field--pick-<?php echo esc_attr( $layout ); ?><?php echo esc_attr( $this->extra_classes( $field ) ); ?>"<?php echo $this->wrapper_data( $field ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- already escaped in wrapper_data().?>>
    … …  
    182185                                                        name="<?php echo esc_attr( $this->input_name( $field ) ); ?>"
    183186                                                        value="<?php echo esc_attr( $choice['value'] ); ?>"
    184                                                         <?php checked( true, $choice['default'] && ! $choice['disabled'] ); ?>
     187                                                        <?php checked( true, $this->choice_on( $choice, $sticky ) && ! $choice['disabled'] ); ?>
    185188                                                        <?php disabled( true, $choice['disabled'] ); ?>
    186189                                                        <?php echo empty( $field['required'] ) ? '' : 'required aria-required="true"'; ?>
  • 2ranq-forms/tags/1.2.0/includes/fields/class-ranq-forms-field-name.php

    r3681524 r3763733  
    9999                $describedby = $this->describedby( $field, $form_id );
    100100                $base        = $this->input_id( $field, $form_id );
     101
     102                // After a failed send the visitor's name comes back instead of blank boxes.
     103                $sticky = $this->sticky_value( $field, $form_id );
    101104                ?>
    102105                <div class="ranq-field ranq-field--name<?php echo esc_attr( $this->extra_classes( $field ) ); ?>"<?php echo $this->wrapper_data( $field ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- already escaped in wrapper_data().?>>
    … …  
    120123                                                                class="ranq-field__input"
    121124                                                                autocomplete="<?php echo esc_attr( $part[1] ); ?>"
     125                                                                <?php if ( is_array( $sticky ) && isset( $sticky[ $key ] ) && '' !== $sticky[ $key ] ) : ?>
     126                                                                        value="<?php echo esc_attr( $sticky[ $key ] ); ?>"
     127                                                                <?php endif; ?>
    122128                                                                <?php echo empty( $field['required'] ) ? '' : 'required aria-required="true"'; ?>
    123129                                                        />
  • 2ranq-forms/tags/1.2.0/includes/fields/class-ranq-forms-field-number.php

    r3681524 r3763733  
    9494                $has_min     = isset( $field['min'] ) && '' !== $field['min'];
    9595                $has_max     = isset( $field['max'] ) && '' !== $field['max'];
     96
     97                // After a failed send the visitor's number comes back instead of a blank box.
     98                $sticky = $this->sticky_value( $field, $form_id );
    9699                ?>
    97100                <div class="ranq-field ranq-field--number<?php echo esc_attr( $this->extra_classes( $field ) ); ?>"<?php echo $this->wrapper_data( $field ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- already escaped in wrapper_data().?>>
    … …  
    103106                                class="ranq-field__input"
    104107                                inputmode="decimal"
     108                                <?php if ( is_string( $sticky ) && '' !== $sticky ) : ?>
     109                                        value="<?php echo esc_attr( $sticky ); ?>"
     110                                <?php endif; ?>
    105111                                <?php if ( $has_min ) : ?>
    106112                                        min="<?php echo esc_attr( (string) $field['min'] ); ?>"
  • 2ranq-forms/tags/1.2.0/includes/fields/class-ranq-forms-field-password.php

    r3681524 r3763733  
    7171
    7272        /**
     73         * A password never comes back into the page, not even after a failed send.
     74         *
     75         * Every other field gives the visitor their text back, this one must not: a
     76         * value printed into the HTML can end up in a cache, in a saved copy of the
     77         * page, or on a shared screen. Typing it again is the smaller harm.
     78         *
     79         * @param array $field   The field settings.
     80         * @param int   $form_id The form id.
     81         *
     82         * @return null
     83         */
     84        protected function sticky_value( $field, $form_id ) {
     85
     86                unset( $field, $form_id );
     87
     88                return null;
     89        }
     90
     91        /**
    7392         * A password is NEVER shown in the entry or in the email.
    7493         *
  • 2ranq-forms/tags/1.2.0/includes/fields/class-ranq-forms-field-phone.php

    r3681524 r3763733  
    286286                $name        = $this->input_name( $field );
    287287                $guess       = $this->guess_country( $field );
     288
     289                // After a failed send the visitor's country and number come back. sanitize()
     290                // always fills the country in, so an empty one only means a fresh form.
     291                $sticky  = $this->sticky_value( $field, $form_id );
     292                $country = is_array( $sticky ) && ! empty( $sticky['country'] ) ? $sticky['country'] : $guess;
     293                $number  = is_array( $sticky ) && isset( $sticky['number'] )
     294                        ? (string) $sticky['number']
     295                        : ( empty( $field['default_value'] ) ? '' : (string) $field['default_value'] );
    288296                ?>
    289297                <div class="ranq-field ranq-field--phone ranq-field--phone-smart<?php echo esc_attr( $this->extra_classes( $field ) ); ?>"<?php echo $this->wrapper_data( $field ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- already escaped in wrapper_data().?>>
    … …  
    310318                                                        data-trunk="<?php echo esc_attr( $country['trunk'] ); ?>"
    311319                                                        data-name="<?php echo esc_attr( $country['name'] ); ?>"
    312                                                         <?php selected( $code, $guess ); ?>
     320                                                        <?php selected( $code, $country ); ?>
    313321                                                >
    314322                                                        <?php echo esc_html( $code . ' +' . $country['dial'] . ' ' . $country['name'] ); ?>
    … …  
    326334                                                placeholder="<?php echo esc_attr( $field['placeholder'] ); ?>"
    327335                                        <?php endif; ?>
    328                                         <?php if ( ! empty( $field['default_value'] ) ) : ?>
    329                                                 value="<?php echo esc_attr( $field['default_value'] ); ?>"
     336                                        <?php if ( '' !== $number ) : ?>
     337                                                value="<?php echo esc_attr( $number ); ?>"
    330338                                        <?php endif; ?>
    331339                                        <?php if ( $describedby ) : ?>
  • 2ranq-forms/tags/1.2.0/includes/fields/class-ranq-forms-field-radio.php

    r3681524 r3763733  
    6666                $choices     = $this->get_choices( $field );
    6767                $base_id     = $this->input_id( $field, $form_id );
     68
     69                // After a failed send the visitor's pick comes back instead of the default.
     70                $sticky = $this->sticky_value( $field, $form_id );
    6871                ?>
    6972                <div class="ranq-field ranq-field--radio<?php echo esc_attr( $this->extra_classes( $field ) ); ?>"<?php echo $this->wrapper_data( $field ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- already escaped in wrapper_data().?>>
    … …  
    9295                                                        name="<?php echo esc_attr( $this->input_name( $field ) ); ?>"
    9396                                                        value="<?php echo esc_attr( $choice['value'] ); ?>"
    94                                                         <?php checked( true, $choice['default'] ); ?>
     97                                                        <?php checked( true, $this->choice_on( $choice, $sticky ) ); ?>
    9598                                                        <?php echo empty( $field['required'] ) ? '' : 'required aria-required="true"'; ?>
    9699                                                />
  • 2ranq-forms/tags/1.2.0/includes/fields/class-ranq-forms-field-ranking.php

    r3681524 r3763733  
    7373                $base_id     = $this->input_id( $field, $form_id );
    7474                $ukupno      = count( $choices );
     75
     76                // After a failed send the visitor's order comes back instead of the one
     77                // the owner wrote the options in.
     78                $sticky = $this->sticky_value( $field, $form_id );
    7579                ?>
    7680                <div class="ranq-field ranq-field--ranking<?php echo esc_attr( $this->extra_classes( $field ) ); ?>"<?php echo $this->wrapper_data( $field ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- already escaped in wrapper_data().?>>
    … …  
    112116                                                                        class="ranq-rank__input"
    113117                                                                        name="<?php echo esc_attr( $this->input_name( $field ) ); ?>[<?php echo esc_attr( $choice['value'] ); ?>]"
    114                                                                         value="<?php echo esc_attr( (string) ( $index + 1 ) ); ?>"
     118                                                                        value="<?php echo esc_attr( (string) ( is_array( $sticky ) && isset( $sticky[ $choice['value'] ] ) ? (int) $sticky[ $choice['value'] ] : $index + 1 ) ); ?>"
    115119                                                                        min="1"
    116120                                                                        max="<?php echo esc_attr( (string) $ukupno ); ?>"
  • 2ranq-forms/tags/1.2.0/includes/fields/class-ranq-forms-field-rating.php

    r3681524 r3763733  
    110110                $base        = $this->input_id( $field, $form_id );
    111111                $scale       = $this->scale( $field );
     112
     113                // After a failed send the visitor's stars come back instead of none.
     114                $sticky = $this->sticky_value( $field, $form_id );
    112115                ?>
    113116                <div class="ranq-field ranq-field--rating<?php echo esc_attr( $this->extra_classes( $field ) ); ?>"<?php echo $this->wrapper_data( $field ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- already escaped in wrapper_data().?>>
    … …  
    130133                                                        name="<?php echo esc_attr( $this->input_name( $field ) ); ?>"
    131134                                                        value="<?php echo esc_attr( (string) $i ); ?>"
     135                                                        <?php checked( true, is_string( $sticky ) && (string) $i === $sticky ); ?>
    132136                                                        <?php echo empty( $field['required'] ) ? '' : 'required aria-required="true"'; ?>
    133137                                                />
  • 2ranq-forms/tags/1.2.0/includes/fields/class-ranq-forms-field-select.php

    r3681524 r3763733  
    7272                $describedby = $this->describedby( $field, $form_id );
    7373                $choices     = $this->get_choices( $field );
     74
     75                // After a failed send the visitor's pick comes back instead of the default.
     76                $sticky = $this->sticky_value( $field, $form_id );
    7477                ?>
    7578                <div class="ranq-field ranq-field--select<?php echo esc_attr( $this->extra_classes( $field ) ); ?>"<?php echo $this->wrapper_data( $field ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- already escaped in wrapper_data().?>>
    … …  
    8891                                        <option
    8992                                                value="<?php echo esc_attr( $choice['value'] ); ?>"
    90                                                 <?php selected( true, $choice['default'] ); ?>
     93                                                <?php selected( true, $this->choice_on( $choice, $sticky ) ); ?>
    9194                                        >
    9295                                                <?php echo esc_html( $choice['label'] ); ?>
  • 2ranq-forms/tags/1.2.0/includes/fields/class-ranq-forms-field-text.php

    r3681524 r3763733  
    8080
    8181                $describedby = $this->describedby( $field, $form_id );
     82
     83                // After a failed send the visitor's text comes back instead of a blank box.
     84                $sticky = $this->sticky_value( $field, $form_id );
    8285                ?>
    8386                <div class="ranq-field ranq-field--text<?php echo esc_attr( $this->extra_classes( $field ) ); ?>"<?php echo $this->wrapper_data( $field ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- already escaped in wrapper_data().?>>
    … …  
    8891                                name="<?php echo esc_attr( $this->input_name( $field ) ); ?>"
    8992                                class="ranq-field__input"
     93                                <?php if ( is_string( $sticky ) && '' !== $sticky ) : ?>
     94                                        value="<?php echo esc_attr( $sticky ); ?>"
     95                                <?php endif; ?>
    9096                                <?php if ( ! empty( $field['placeholder'] ) ) : ?>
    9197                                        placeholder="<?php echo esc_attr( $field['placeholder'] ); ?>"
  • 2ranq-forms/tags/1.2.0/includes/fields/class-ranq-forms-field-textarea.php

    r3681524 r3763733  
    102102                $describedby = $this->describedby( $field, $form_id );
    103103                $rows        = isset( $field['rows'] ) ? absint( $field['rows'] ) : 5;
     104
     105                // After a failed send the visitor's text comes back. This is the field
     106                // where losing it hurts the most: the message itself lives here.
     107                $sticky = $this->sticky_value( $field, $form_id );
    104108                ?>
    105109                <div class="ranq-field ranq-field--textarea<?php echo esc_attr( $this->extra_classes( $field ) ); ?>"<?php echo $this->wrapper_data( $field ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- already escaped in wrapper_data().?>>
    … …  
    120124                                <?php endif; ?>
    121125                                <?php echo empty( $field['required'] ) ? '' : 'required aria-required="true"'; ?>
    122                         ></textarea>
     126                        ><?php echo is_string( $sticky ) ? esc_textarea( $sticky ) : ''; ?></textarea>
    123127                        <?php $this->render_description( $field, $form_id ); ?>
    124128                </div>
  • 2ranq-forms/tags/1.2.0/readme.txt

    r3688493 r3763733  
    55Tested up to: 7.1
    66Requires PHP: 7.4
    7 Stable tag: 1.1.0
     7Stable tag: 1.2.0
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    9393Privacy policy: https://www.hcaptcha.com/privacy
    9494
     95**Cloudflare Turnstile**
     96
     97Used for the same job as reCAPTCHA, usually without showing the visitor anything
     98to click.
     99
     100The Turnstile script (https://challenges.cloudflare.com/turnstile/v0/api.js) is
     101loaded in the visitor's browser on every page that shows a protected form, which
     102lets Cloudflare see the visitor's IP address and browser. When the form is
     103submitted, the token and your secret key are sent from your server to
     104https://challenges.cloudflare.com/turnstile/v0/siteverify to be verified. The
     105answers typed into the form are never sent.
     106
     107Service provided by Cloudflare, Inc. Terms of service:
     108https://www.cloudflare.com/terms/
     109Privacy policy: https://www.cloudflare.com/privacypolicy/
     110
    95111**Mailchimp**
    96112
    … …  
    157173
    158174No. A hidden trap field and a rate limit are always on and need nothing from
    159 outside. Google reCAPTCHA and hCaptcha are there if you want them, and both are
    160 off until you enter your own keys.
     175outside. Google reCAPTCHA, hCaptcha and Cloudflare Turnstile are there if you
     176want them, and all three are off until you enter your own keys.
    161177
    162178= Where are uploaded files kept? =
    … …  
    205221
    206222== Changelog ==
     223
     224= 1.2.0 =
     225* A send from a cached page no longer disappears. The security token in a form dies after a day, while a cache plugin can serve the same page for a week, and a visitor sending from such a page was thrown away without a word and without their text. The token is now only asked of logged-in visitors, where it does its real CSRF job. A public form is guarded by the hidden trap field and the rate limit, which do not age with the page. Contact Form 7 and Gravity Forms handle a public form the same way.
     226* No rejected send is silent any more. When a check says no, the visitor is told on the page, and every field gives them back what they typed: text, choices, ticks, stars, ranking order, name, address, phone. A password is never printed back, and a browser does not allow a file to be put back into a file box.
     227* Cloudflare Turnstile is back as the third spam check, next to Google reCAPTCHA and hCaptcha. It sat out the first release over a false positive in the directory's machine check, not over anything in how it works. Off until you enter your own keys, like the other two.
    207228
    208229= 1.1.0 =
    … …  
    235256== Upgrade Notice ==
    236257
     258= 1.2.0 =
     259Recommended for every site with page caching. A visitor sending from a cached page is no longer silently thrown away, every rejected send now shows a message and keeps the typed values, and Cloudflare Turnstile joins reCAPTCHA and hCaptcha.
     260
    237261= 1.1.0 =
    238262Recommended. Notifications now arrive as a card with a frame colour you choose, the link opens the entry itself, and answers to a dropdown, a radio button or a checkbox read as the visitor saw them.
  • 2ranq-forms/trunk/2ranq-forms.php

    r3688493 r3763733  
    44 * Plugin URI:        https://2ranq.com/plugins/2ranq-forms/
    55 * Description:       Form builder with conditional logic, multi-step forms and file uploads. Every feature free, nothing locked.
    6  * Version:           1.1.0
     6 * Version:           1.2.0
    77 * Requires at least: 6.6
    88 * Requires PHP:      7.4
    … …  
    3333 */
    3434
    35 define( 'RANQ_FORMS_VERSION', '1.1.0' );
     35define( 'RANQ_FORMS_VERSION', '1.2.0' );
    3636define( 'RANQ_FORMS_FILE', __FILE__ );
    3737define( 'RANQ_FORMS_DIR', plugin_dir_path( __FILE__ ) );
  • 2ranq-forms/trunk/includes/class-ranq-forms-spam.php

    r3681524 r3763733  
    33 * Spam protection, with the keys of outside services.
    44 *
    5  * Google reCAPTCHA (v2 checkbox, v2 invisible, v3) and hCaptcha, all unlocked.
    6  * The owner enters the keys once, and every form can use them.
    7  *
    8  * Cloudflare Turnstile WORKED and was taken out of the first release, because
    9  * the official Plugin Check flags every `cloudflare.com` and rejects the
    10  * submission by machine. The whole removed code and the procedure for putting it
    11  * back are in `13-TURNSTILE-VRACANJE.md`.
     5 * Google reCAPTCHA (v2 checkbox, v2 invisible, v3), hCaptcha and Cloudflare
     6 * Turnstile, all unlocked. The owner enters the keys once, and every form can
     7 * use them.
     8 *
     9 * Turnstile sat out the first release: the Plugin Check "offloading" sniff
     10 * flags every `cloudflare.com` address because Cloudflare also serves other
     11 * people's files, and an error of that level rejects a NEW submission by
     12 * machine. Turnstile is not file offloading, it is a visitor check, the same
     13 * as the two services above it that the sniff does not flag. It came back in
     14 * 1.2.0, with the sniff told so on each of the two addresses.
    1215 *
    1316 * The check runs ON THE SERVER. Without that the protection is worth nothing,
    … …  
    8184                                'script' => 'https://js.hcaptcha.com/1/api.js',
    8285                        ),
     86                        'turnstile' => array(
     87                                'name'   => __( 'Cloudflare Turnstile', '2ranq-forms' ),
     88                                'note'   => __( 'Free, quiet, and usually invisible to the visitor. The easiest of the three.', '2ranq-forms' ),
     89                                // The dashboard address under the button the owner presses to go and
     90                                // make their keys, the same as the two services above have. Nothing
     91                                // is loaded from it, so it is not offloading either.
     92                                'link'   => 'https://dash.cloudflare.com/?to=/:account/turnstile', // phpcs:ignore PluginCheck.CodeAnalysis.Offloading.OffloadedContent
     93                                'link_text' => __( 'Open Turnstile in Cloudflare', '2ranq-forms' ),
     94                                'steps'  => array(
     95                                        __( 'Make a free Cloudflare account. Your site does not have to use Cloudflare for anything else.', '2ranq-forms' ),
     96                                        __( 'Open Turnstile, press Add widget, type your domain, and leave the mode on Managed.', '2ranq-forms' ),
     97                                        __( 'Copy the Site Key and the Secret Key into the two boxes below.', '2ranq-forms' ),
     98                                ),
     99                                // The two addresses below are the visitor check itself, not content
     100                                // offloading: nothing of the site is served from them, they only
     101                                // answer whether the visitor passed. The sniff cannot tell the two
     102                                // apart because both live on cloudflare.com, so it is told here.
     103                                'verify' => 'https://challenges.cloudflare.com/turnstile/v0/siteverify', // phpcs:ignore PluginCheck.CodeAnalysis.Offloading.OffloadedContent
     104                                'script' => 'https://challenges.cloudflare.com/turnstile/v0/api.js', // phpcs:ignore PluginCheck.CodeAnalysis.Offloading.OffloadedContent
     105                        ),
    83106                );
    84107        }
    … …  
    221244                }
    222245
    223                 $klasa = 'hcaptcha' === $s['provider'] ? 'h-captcha' : 'g-recaptcha';
     246                $klasa = 'hcaptcha' === $s['provider'] ? 'h-captcha' : ( 'turnstile' === $s['provider'] ? 'cf-turnstile' : 'g-recaptcha' );
    224247                $extra = '';
    225248
    … …  
    304327                if ( '' === $odgovor ) {
    305328                        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- the same.
     329                        $odgovor = isset( $_POST['cf-turnstile-response'] ) ? sanitize_text_field( wp_unslash( $_POST['cf-turnstile-response'] ) ) : '';
     330                }
     331
     332                if ( '' === $odgovor ) {
     333                        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- the same.
    306334                        $odgovor = isset( $_POST['ranq_spam_token'] ) ? sanitize_text_field( wp_unslash( $_POST['ranq_spam_token'] ) ) : '';
    307335                }
  • 2ranq-forms/trunk/includes/class-ranq-forms-submission.php

    r3681524 r3763733  
    4444                }
    4545
    46                 if ( ! isset( $_POST['ranq_forms_id'], $_POST['ranq_forms_nonce'] ) ) {
     46                if ( ! isset( $_POST['ranq_forms_id'] ) ) {
    4747                        return;
    4848                }
    … …  
    5151
    5252                if ( ! $form_id ) {
    53                         return;
    54                 }
    55 
    56                 $nonce = sanitize_text_field( wp_unslash( $_POST['ranq_forms_nonce'] ) );
    57 
    58                 if ( ! wp_verify_nonce( $nonce, 'ranq_forms_submit_' . $form_id ) ) {
    59                         return;
    60                 }
    61 
    62                 // The bot trap. The field sits in the form, moved off the screen, and a person
    63                 // neither sees it nor can fill it in with the keyboard. A robot that fills in
    64                 // everything it finds fills that in too. It is rejected quietly, with no
    65                 // message, because a message tells the robot where the mistake is.
    66                 if ( ! empty( $_POST['ranq_hp'] ) ) {
    6753                        return;
    6854                }
    … …  
    7258                // swallow the error and the visitor would stare at a blank form with nothing.
    7359                self::$submitted = $form_id;
     60
     61                // The nonce is only asked of a logged-in visitor, because only there it does
     62                // its real job: it stops another site from riding that visitor's session. A
     63                // guest has no session to ride, so for a guest the nonce protected nothing a
     64                // robot could not get around by fetching a fresh page, and it did real harm:
     65                // a cache plugin happily serves the same page for a week while the nonce in
     66                // it dies after a day, so a visitor sending from a cached page was thrown
     67                // away without a word. Contact Form 7 and Gravity Forms treat a public form
     68                // the same way. What actually guards a public form are the bot trap and the
     69                // rate limit below, which do not age with the page.
     70                if ( is_user_logged_in() ) {
     71
     72                        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- the nonce is only compared, never stored or printed.
     73                        $nonce = isset( $_POST['ranq_forms_nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['ranq_forms_nonce'] ) ) : '';
     74
     75                        if ( ! wp_verify_nonce( $nonce, 'ranq_forms_submit_' . $form_id ) ) {
     76
     77                                // Never silently. The page the visitor sent from had simply been open,
     78                                // or cached, for longer than the nonce lives. They get told, and the
     79                                // form below gives them their typed values back, so nothing is lost.
     80                                self::$errors = array( '_form' => __( 'This page had been open for too long. Please send the form again.', '2ranq-forms' ) );
     81
     82                                return;
     83                        }
     84                }
     85
     86                // The bot trap. The field sits in the form, moved off the screen, and a person
     87                // neither sees it nor can fill it in with the keyboard. A robot that fills in
     88                // everything it finds fills that in too. The message is deliberately vague, it
     89                // does not say which check failed, so a robot learns nothing from it. But it
     90                // is no longer silent, because a browser extension that autofills every field
     91                // it finds can trip the trap for a real person. That person gets their text
     92                // back, the trap field renders empty again, and the second send goes through.
     93                if ( ! empty( $_POST['ranq_hp'] ) ) {
     94                        self::$errors = array( '_form' => __( 'The form could not be sent. Please try again.', '2ranq-forms' ) );
     95
     96                        return;
     97                }
    7498
    7599                // The rate limit. Without this one person can fill the database and, through
    … …  
    286310
    287311                return self::$sent && self::$submitted === (int) $form_id;
     312        }
     313
     314        /**
     315         * What the visitor sent for one field, raw, or null.
     316         *
     317         * It only answers while THIS form's send has just failed, so the fields can
     318         * give the visitor their typed values back instead of a blank form. Any other
     319         * time, for any other form on the page, and after a send that went through,
     320         * it stays quiet and the field falls back to its default. The value comes
     321         * back raw on purpose: only the field's own class knows how to clean it, so
     322         * cleaning happens there, never here.
     323         *
     324         * @param int    $form_id The form id.
     325         * @param string $key     The field key.
     326         *
     327         * @return mixed The raw sent value, or null when there is nothing to give back.
     328         */
     329        public static function posted_raw( $form_id, $key ) {
     330
     331                if ( self::$sent || self::$submitted !== (int) $form_id ) {
     332                        return null;
     333                }
     334
     335                // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- read only to hand the visitor's text back to the field, which cleans it with its own sanitize() before printing.
     336                $raw = isset( $_POST['ranq_fields'] ) && is_array( $_POST['ranq_fields'] ) ? wp_unslash( $_POST['ranq_fields'] ) : array();
     337
     338                return isset( $raw[ $key ] ) ? $raw[ $key ] : null;
    288339        }
    289340
  • 2ranq-forms/trunk/includes/fields/abstract-ranq-forms-field-choices.php

    r3688493 r3763733  
    107107
    108108        /**
     109         * Whether one choice renders as picked.
     110         *
     111         * While the form is fresh the owner's defaults decide. The moment a send has
     112         * failed, what the visitor picked wins, even when they picked nothing, because
     113         * putting the defaults back would quietly change their answer under them.
     114         *
     115         * @param array $choice One choice, as get_choices() returns it.
     116         * @param mixed $sticky What sticky_value() gave for the whole field.
     117         *
     118         * @return bool
     119         */
     120        protected function choice_on( $choice, $sticky ) {
     121
     122                if ( null === $sticky ) {
     123                        return ! empty( $choice['default'] );
     124                }
     125
     126                $picked = array_map( 'strval', array_filter( is_array( $sticky ) ? $sticky : array( $sticky ), 'is_scalar' ) );
     127
     128                return in_array( (string) $choice['value'], $picked, true );
     129        }
     130
     131        /**
    109132         * The value prepared for display, as the label the visitor actually saw.
    110133         *
  • 2ranq-forms/trunk/includes/fields/abstract-ranq-forms-field-input.php

    r3681524 r3763733  
    7777                $mode        = $this->input_mode();
    7878                $auto        = $this->autocomplete();
     79
     80                // After a failed send the visitor's own text comes back, otherwise the
     81                // owner's default. The two never mix: a failed send with an emptied field
     82                // stays empty, because the default would change the answer under them.
     83                $sticky = $this->sticky_value( $field, $form_id );
     84                $value  = is_string( $sticky )
     85                        ? $sticky
     86                        : ( empty( $field['default_value'] ) ? '' : (string) $field['default_value'] );
    7987                ?>
    8088                <div class="ranq-field ranq-field--<?php echo esc_attr( $this->get_type() ); ?><?php echo esc_attr( $this->extra_classes( $field ) ); ?>"<?php echo $this->wrapper_data( $field ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- already escaped in wrapper_data().?>>
    … …  
    94102                                        placeholder="<?php echo esc_attr( $field['placeholder'] ); ?>"
    95103                                <?php endif; ?>
    96                                 <?php if ( ! empty( $field['default_value'] ) ) : ?>
    97                                         value="<?php echo esc_attr( $field['default_value'] ); ?>"
     104                                <?php if ( '' !== $value ) : ?>
     105                                        value="<?php echo esc_attr( $value ); ?>"
    98106                                <?php endif; ?>
    99107                                <?php if ( $describedby ) : ?>
  • 2ranq-forms/trunk/includes/fields/abstract-ranq-forms-field.php

    r3681524 r3763733  
    329329
    330330        /**
     331         * The value the field shows when the form comes back after a failed send.
     332         *
     333         * A send can fail long after the visitor wrote their text: a required field
     334         * was missed, the spam check said no, the page had been cached for a week. A
     335         * blank form at that moment throws the whole message away, and nobody types
     336         * it twice. So every field that can, gives the text back. The value is
     337         * cleaned by the field's own sanitize(), because what came in is as raw as
     338         * any other sent value. Null means there is nothing to give back, and the
     339         * field falls back to its default. A password and a file stay out of this on
     340         * purpose: a password is never printed back into a page, and a browser does
     341         * not allow a file input to be prefilled at all.
     342         *
     343         * @param array $field   The field settings.
     344         * @param int   $form_id The form id.
     345         *
     346         * @return mixed The cleaned sent value, or null when there is none.
     347         */
     348        protected function sticky_value( $field, $form_id ) {
     349
     350                $raw = Ranq_Forms_Submission::posted_raw( $form_id, $this->field_key( $field ) );
     351
     352                if ( null === $raw ) {
     353                        return null;
     354                }
     355
     356                return $this->sanitize( $raw, $field );
     357        }
     358
     359        /**
    331360         * The element id in the HTML, has to be unique per form.
    332361         *
  • 2ranq-forms/trunk/includes/fields/class-ranq-forms-field-address.php

    r3681524 r3763733  
    103103                $describedby = $this->describedby( $field, $form_id );
    104104                $base        = $this->input_id( $field, $form_id );
     105
     106                // After a failed send the visitor's address comes back instead of blank boxes.
     107                $sticky = $this->sticky_value( $field, $form_id );
    105108                ?>
    106109                <div class="ranq-field ranq-field--address<?php echo esc_attr( $this->extra_classes( $field ) ); ?>"<?php echo $this->wrapper_data( $field ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- already escaped in wrapper_data().?>>
    … …  
    124127                                                                class="ranq-field__input"
    125128                                                                autocomplete="<?php echo esc_attr( $part[1] ); ?>"
     129                                                                <?php if ( is_array( $sticky ) && isset( $sticky[ $key ] ) && '' !== $sticky[ $key ] ) : ?>
     130                                                                        value="<?php echo esc_attr( $sticky[ $key ] ); ?>"
     131                                                                <?php endif; ?>
    126132                                                        />
    127133                                                        <label class="ranq-field__sublabel" for="<?php echo esc_attr( $base . '-' . $key ); ?>">
  • 2ranq-forms/trunk/includes/fields/class-ranq-forms-field-checkbox.php

    r3681524 r3763733  
    8686                $choices     = $this->get_choices( $field );
    8787                $base_id     = $this->input_id( $field, $form_id );
     88
     89                // After a failed send the visitor's ticks come back instead of the defaults.
     90                $sticky = $this->sticky_value( $field, $form_id );
    8891                ?>
    8992                <div class="ranq-field ranq-field--checkbox<?php echo esc_attr( $this->extra_classes( $field ) ); ?>"<?php echo $this->wrapper_data( $field ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- already escaped in wrapper_data().?>>
    … …  
    112115                                                        name="<?php echo esc_attr( $this->input_name( $field ) ); ?>"
    113116                                                        value="<?php echo esc_attr( $choice['value'] ); ?>"
    114                                                         <?php checked( true, $choice['default'] ); ?>
     117                                                        <?php checked( true, $this->choice_on( $choice, $sticky ) ); ?>
    115118                                                />
    116119                                                <label for="<?php echo esc_attr( $choice_id ); ?>">
  • 2ranq-forms/trunk/includes/fields/class-ranq-forms-field-email.php

    r3681524 r3763733  
    6666
    6767        /**
     68         * What comes back into the box after a failed send.
     69         *
     70         * sanitize_email() throws away anything that is not a whole address, and the
     71         * most common reason this very field fails is a half-typed address. Giving
     72         * back an empty box would hide the typo the visitor needs to see to fix it.
     73         * So what comes back is cleaned as plain text instead, escaped on printing
     74         * like every other value, while the strict cleaning above still guards
     75         * everything that gets stored or mailed.
     76         *
     77         * @param array $field   The field settings.
     78         * @param int   $form_id The form id.
     79         *
     80         * @return mixed The sent text, or null when there is none.
     81         */
     82        protected function sticky_value( $field, $form_id ) {
     83
     84                $raw = Ranq_Forms_Submission::posted_raw( $form_id, $this->field_key( $field ) );
     85
     86                if ( null === $raw ) {
     87                        return null;
     88                }
     89
     90                return sanitize_text_field( is_scalar( $raw ) ? (string) $raw : '' );
     91        }
     92
     93        /**
    6894         * Prints the field.
    6995         *
    … …  
    76102
    77103                $describedby = $this->describedby( $field, $form_id );
     104
     105                // After a failed send the visitor's address comes back instead of a blank box.
     106                $sticky = $this->sticky_value( $field, $form_id );
    78107                ?>
    79108                <div class="ranq-field ranq-field--email<?php echo esc_attr( $this->extra_classes( $field ) ); ?>"<?php echo $this->wrapper_data( $field ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- already escaped in wrapper_data().?>>
    … …  
    86115                                autocomplete="email"
    87116                                inputmode="email"
     117                                <?php if ( is_string( $sticky ) && '' !== $sticky ) : ?>
     118                                        value="<?php echo esc_attr( $sticky ); ?>"
     119                                <?php endif; ?>
    88120                                <?php if ( ! empty( $field['placeholder'] ) ) : ?>
    89121                                        placeholder="<?php echo esc_attr( $field['placeholder'] ); ?>"
  • 2ranq-forms/trunk/includes/fields/class-ranq-forms-field-imagechoice.php

    r3681524 r3763733  
    155155                $base_id     = $this->input_id( $field, $form_id );
    156156                $layout      = isset( $field['picture_layout'] ) && 'list' === $field['picture_layout'] ? 'list' : 'grid';
     157
     158                // After a failed send the visitor's pick comes back instead of the default.
     159                $sticky = $this->sticky_value( $field, $form_id );
    157160                ?>
    158161                <div class="ranq-field ranq-field--imagechoice ranq-field--pick-<?php echo esc_attr( $layout ); ?><?php echo esc_attr( $this->extra_classes( $field ) ); ?>"<?php echo $this->wrapper_data( $field ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- already escaped in wrapper_data().?>>
    … …  
    182185                                                        name="<?php echo esc_attr( $this->input_name( $field ) ); ?>"
    183186                                                        value="<?php echo esc_attr( $choice['value'] ); ?>"
    184                                                         <?php checked( true, $choice['default'] && ! $choice['disabled'] ); ?>
     187                                                        <?php checked( true, $this->choice_on( $choice, $sticky ) && ! $choice['disabled'] ); ?>
    185188                                                        <?php disabled( true, $choice['disabled'] ); ?>
    186189                                                        <?php echo empty( $field['required'] ) ? '' : 'required aria-required="true"'; ?>
  • 2ranq-forms/trunk/includes/fields/class-ranq-forms-field-name.php

    r3681524 r3763733  
    9999                $describedby = $this->describedby( $field, $form_id );
    100100                $base        = $this->input_id( $field, $form_id );
     101
     102                // After a failed send the visitor's name comes back instead of blank boxes.
     103                $sticky = $this->sticky_value( $field, $form_id );
    101104                ?>
    102105                <div class="ranq-field ranq-field--name<?php echo esc_attr( $this->extra_classes( $field ) ); ?>"<?php echo $this->wrapper_data( $field ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- already escaped in wrapper_data().?>>
    … …  
    120123                                                                class="ranq-field__input"
    121124                                                                autocomplete="<?php echo esc_attr( $part[1] ); ?>"
     125                                                                <?php if ( is_array( $sticky ) && isset( $sticky[ $key ] ) && '' !== $sticky[ $key ] ) : ?>
     126                                                                        value="<?php echo esc_attr( $sticky[ $key ] ); ?>"
     127                                                                <?php endif; ?>
    122128                                                                <?php echo empty( $field['required'] ) ? '' : 'required aria-required="true"'; ?>
    123129                                                        />
  • 2ranq-forms/trunk/includes/fields/class-ranq-forms-field-number.php

    r3681524 r3763733  
    9494                $has_min     = isset( $field['min'] ) && '' !== $field['min'];
    9595                $has_max     = isset( $field['max'] ) && '' !== $field['max'];
     96
     97                // After a failed send the visitor's number comes back instead of a blank box.
     98                $sticky = $this->sticky_value( $field, $form_id );
    9699                ?>
    97100                <div class="ranq-field ranq-field--number<?php echo esc_attr( $this->extra_classes( $field ) ); ?>"<?php echo $this->wrapper_data( $field ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- already escaped in wrapper_data().?>>
    … …  
    103106                                class="ranq-field__input"
    104107                                inputmode="decimal"
     108                                <?php if ( is_string( $sticky ) && '' !== $sticky ) : ?>
     109                                        value="<?php echo esc_attr( $sticky ); ?>"
     110                                <?php endif; ?>
    105111                                <?php if ( $has_min ) : ?>
    106112                                        min="<?php echo esc_attr( (string) $field['min'] ); ?>"
  • 2ranq-forms/trunk/includes/fields/class-ranq-forms-field-password.php

    r3681524 r3763733  
    7171
    7272        /**
     73         * A password never comes back into the page, not even after a failed send.
     74         *
     75         * Every other field gives the visitor their text back, this one must not: a
     76         * value printed into the HTML can end up in a cache, in a saved copy of the
     77         * page, or on a shared screen. Typing it again is the smaller harm.
     78         *
     79         * @param array $field   The field settings.
     80         * @param int   $form_id The form id.
     81         *
     82         * @return null
     83         */
     84        protected function sticky_value( $field, $form_id ) {
     85
     86                unset( $field, $form_id );
     87
     88                return null;
     89        }
     90
     91        /**
    7392         * A password is NEVER shown in the entry or in the email.
    7493         *
  • 2ranq-forms/trunk/includes/fields/class-ranq-forms-field-phone.php

    r3681524 r3763733  
    286286                $name        = $this->input_name( $field );
    287287                $guess       = $this->guess_country( $field );
     288
     289                // After a failed send the visitor's country and number come back. sanitize()
     290                // always fills the country in, so an empty one only means a fresh form.
     291                $sticky  = $this->sticky_value( $field, $form_id );
     292                $country = is_array( $sticky ) && ! empty( $sticky['country'] ) ? $sticky['country'] : $guess;
     293                $number  = is_array( $sticky ) && isset( $sticky['number'] )
     294                        ? (string) $sticky['number']
     295                        : ( empty( $field['default_value'] ) ? '' : (string) $field['default_value'] );
    288296                ?>
    289297                <div class="ranq-field ranq-field--phone ranq-field--phone-smart<?php echo esc_attr( $this->extra_classes( $field ) ); ?>"<?php echo $this->wrapper_data( $field ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- already escaped in wrapper_data().?>>
    … …  
    310318                                                        data-trunk="<?php echo esc_attr( $country['trunk'] ); ?>"
    311319                                                        data-name="<?php echo esc_attr( $country['name'] ); ?>"
    312                                                         <?php selected( $code, $guess ); ?>
     320                                                        <?php selected( $code, $country ); ?>
    313321                                                >
    314322                                                        <?php echo esc_html( $code . ' +' . $country['dial'] . ' ' . $country['name'] ); ?>
    … …  
    326334                                                placeholder="<?php echo esc_attr( $field['placeholder'] ); ?>"
    327335                                        <?php endif; ?>
    328                                         <?php if ( ! empty( $field['default_value'] ) ) : ?>
    329                                                 value="<?php echo esc_attr( $field['default_value'] ); ?>"
     336                                        <?php if ( '' !== $number ) : ?>
     337                                                value="<?php echo esc_attr( $number ); ?>"
    330338                                        <?php endif; ?>
    331339                                        <?php if ( $describedby ) : ?>
  • 2ranq-forms/trunk/includes/fields/class-ranq-forms-field-radio.php

    r3681524 r3763733  
    6666                $choices     = $this->get_choices( $field );
    6767                $base_id     = $this->input_id( $field, $form_id );
     68
     69                // After a failed send the visitor's pick comes back instead of the default.
     70                $sticky = $this->sticky_value( $field, $form_id );
    6871                ?>
    6972                <div class="ranq-field ranq-field--radio<?php echo esc_attr( $this->extra_classes( $field ) ); ?>"<?php echo $this->wrapper_data( $field ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- already escaped in wrapper_data().?>>
    … …  
    9295                                                        name="<?php echo esc_attr( $this->input_name( $field ) ); ?>"
    9396                                                        value="<?php echo esc_attr( $choice['value'] ); ?>"
    94                                                         <?php checked( true, $choice['default'] ); ?>
     97                                                        <?php checked( true, $this->choice_on( $choice, $sticky ) ); ?>
    9598                                                        <?php echo empty( $field['required'] ) ? '' : 'required aria-required="true"'; ?>
    9699                                                />
  • 2ranq-forms/trunk/includes/fields/class-ranq-forms-field-ranking.php

    r3681524 r3763733  
    7373                $base_id     = $this->input_id( $field, $form_id );
    7474                $ukupno      = count( $choices );
     75
     76                // After a failed send the visitor's order comes back instead of the one
     77                // the owner wrote the options in.
     78                $sticky = $this->sticky_value( $field, $form_id );
    7579                ?>
    7680                <div class="ranq-field ranq-field--ranking<?php echo esc_attr( $this->extra_classes( $field ) ); ?>"<?php echo $this->wrapper_data( $field ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- already escaped in wrapper_data().?>>
    … …  
    112116                                                                        class="ranq-rank__input"
    113117                                                                        name="<?php echo esc_attr( $this->input_name( $field ) ); ?>[<?php echo esc_attr( $choice['value'] ); ?>]"
    114                                                                         value="<?php echo esc_attr( (string) ( $index + 1 ) ); ?>"
     118                                                                        value="<?php echo esc_attr( (string) ( is_array( $sticky ) && isset( $sticky[ $choice['value'] ] ) ? (int) $sticky[ $choice['value'] ] : $index + 1 ) ); ?>"
    115119                                                                        min="1"
    116120                                                                        max="<?php echo esc_attr( (string) $ukupno ); ?>"
  • 2ranq-forms/trunk/includes/fields/class-ranq-forms-field-rating.php

    r3681524 r3763733  
    110110                $base        = $this->input_id( $field, $form_id );
    111111                $scale       = $this->scale( $field );
     112
     113                // After a failed send the visitor's stars come back instead of none.
     114                $sticky = $this->sticky_value( $field, $form_id );
    112115                ?>
    113116                <div class="ranq-field ranq-field--rating<?php echo esc_attr( $this->extra_classes( $field ) ); ?>"<?php echo $this->wrapper_data( $field ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- already escaped in wrapper_data().?>>
    … …  
    130133                                                        name="<?php echo esc_attr( $this->input_name( $field ) ); ?>"
    131134                                                        value="<?php echo esc_attr( (string) $i ); ?>"
     135                                                        <?php checked( true, is_string( $sticky ) && (string) $i === $sticky ); ?>
    132136                                                        <?php echo empty( $field['required'] ) ? '' : 'required aria-required="true"'; ?>
    133137                                                />
  • 2ranq-forms/trunk/includes/fields/class-ranq-forms-field-select.php

    r3681524 r3763733  
    7272                $describedby = $this->describedby( $field, $form_id );
    7373                $choices     = $this->get_choices( $field );
     74
     75                // After a failed send the visitor's pick comes back instead of the default.
     76                $sticky = $this->sticky_value( $field, $form_id );
    7477                ?>
    7578                <div class="ranq-field ranq-field--select<?php echo esc_attr( $this->extra_classes( $field ) ); ?>"<?php echo $this->wrapper_data( $field ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- already escaped in wrapper_data().?>>
    … …  
    8891                                        <option
    8992                                                value="<?php echo esc_attr( $choice['value'] ); ?>"
    90                                                 <?php selected( true, $choice['default'] ); ?>
     93                                                <?php selected( true, $this->choice_on( $choice, $sticky ) ); ?>
    9194                                        >
    9295                                                <?php echo esc_html( $choice['label'] ); ?>
  • 2ranq-forms/trunk/includes/fields/class-ranq-forms-field-text.php

    r3681524 r3763733  
    8080
    8181                $describedby = $this->describedby( $field, $form_id );
     82
     83                // After a failed send the visitor's text comes back instead of a blank box.
     84                $sticky = $this->sticky_value( $field, $form_id );
    8285                ?>
    8386                <div class="ranq-field ranq-field--text<?php echo esc_attr( $this->extra_classes( $field ) ); ?>"<?php echo $this->wrapper_data( $field ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- already escaped in wrapper_data().?>>
    … …  
    8891                                name="<?php echo esc_attr( $this->input_name( $field ) ); ?>"
    8992                                class="ranq-field__input"
     93                                <?php if ( is_string( $sticky ) && '' !== $sticky ) : ?>
     94                                        value="<?php echo esc_attr( $sticky ); ?>"
     95                                <?php endif; ?>
    9096                                <?php if ( ! empty( $field['placeholder'] ) ) : ?>
    9197                                        placeholder="<?php echo esc_attr( $field['placeholder'] ); ?>"
  • 2ranq-forms/trunk/includes/fields/class-ranq-forms-field-textarea.php

    r3681524 r3763733  
    102102                $describedby = $this->describedby( $field, $form_id );
    103103                $rows        = isset( $field['rows'] ) ? absint( $field['rows'] ) : 5;
     104
     105                // After a failed send the visitor's text comes back. This is the field
     106                // where losing it hurts the most: the message itself lives here.
     107                $sticky = $this->sticky_value( $field, $form_id );
    104108                ?>
    105109                <div class="ranq-field ranq-field--textarea<?php echo esc_attr( $this->extra_classes( $field ) ); ?>"<?php echo $this->wrapper_data( $field ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- already escaped in wrapper_data().?>>
    … …  
    120124                                <?php endif; ?>
    121125                                <?php echo empty( $field['required'] ) ? '' : 'required aria-required="true"'; ?>
    122                         ></textarea>
     126                        ><?php echo is_string( $sticky ) ? esc_textarea( $sticky ) : ''; ?></textarea>
    123127                        <?php $this->render_description( $field, $form_id ); ?>
    124128                </div>
  • 2ranq-forms/trunk/readme.txt

    r3688493 r3763733  
    55Tested up to: 7.1
    66Requires PHP: 7.4
    7 Stable tag: 1.1.0
     7Stable tag: 1.2.0
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    9393Privacy policy: https://www.hcaptcha.com/privacy
    9494
     95**Cloudflare Turnstile**
     96
     97Used for the same job as reCAPTCHA, usually without showing the visitor anything
     98to click.
     99
     100The Turnstile script (https://challenges.cloudflare.com/turnstile/v0/api.js) is
     101loaded in the visitor's browser on every page that shows a protected form, which
     102lets Cloudflare see the visitor's IP address and browser. When the form is
     103submitted, the token and your secret key are sent from your server to
     104https://challenges.cloudflare.com/turnstile/v0/siteverify to be verified. The
     105answers typed into the form are never sent.
     106
     107Service provided by Cloudflare, Inc. Terms of service:
     108https://www.cloudflare.com/terms/
     109Privacy policy: https://www.cloudflare.com/privacypolicy/
     110
    95111**Mailchimp**
    96112
    … …  
    157173
    158174No. A hidden trap field and a rate limit are always on and need nothing from
    159 outside. Google reCAPTCHA and hCaptcha are there if you want them, and both are
    160 off until you enter your own keys.
     175outside. Google reCAPTCHA, hCaptcha and Cloudflare Turnstile are there if you
     176want them, and all three are off until you enter your own keys.
    161177
    162178= Where are uploaded files kept? =
    … …  
    205221
    206222== Changelog ==
     223
     224= 1.2.0 =
     225* A send from a cached page no longer disappears. The security token in a form dies after a day, while a cache plugin can serve the same page for a week, and a visitor sending from such a page was thrown away without a word and without their text. The token is now only asked of logged-in visitors, where it does its real CSRF job. A public form is guarded by the hidden trap field and the rate limit, which do not age with the page. Contact Form 7 and Gravity Forms handle a public form the same way.
     226* No rejected send is silent any more. When a check says no, the visitor is told on the page, and every field gives them back what they typed: text, choices, ticks, stars, ranking order, name, address, phone. A password is never printed back, and a browser does not allow a file to be put back into a file box.
     227* Cloudflare Turnstile is back as the third spam check, next to Google reCAPTCHA and hCaptcha. It sat out the first release over a false positive in the directory's machine check, not over anything in how it works. Off until you enter your own keys, like the other two.
    207228
    208229= 1.1.0 =
    … …  
    235256== Upgrade Notice ==
    236257
     258= 1.2.0 =
     259Recommended for every site with page caching. A visitor sending from a cached page is no longer silently thrown away, every rejected send now shows a message and keeps the typed values, and Cloudflare Turnstile joins reCAPTCHA and hCaptcha.
     260
    237261= 1.1.0 =
    238262Recommended. Notifications now arrive as a card with a frame colour you choose, the link opens the entry itself, and answers to a dropdown, a radio button or a checkbox read as the visitor saw them.
Note: See TracChangeset for help on using the changeset viewer.