PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / trunk
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More vtrunk
6.35 6.34 6.33.1 6.33 6.32.1 6.32 6.31 6.25 6.25.1 6.26 6.26.1 6.27 6.28 6.29 6.3 6.3.1 6.3.2 6.30 6.4 6.4.1 6.4.2 6.5 6.5.1 6.5.2 6.5.3 All 141 releases
← All changes | classes/models/FrmHoneypot.php +237 -37 6.5.2 → trunk View file →
@@ -5,8 +5,26 @@
5 5
6 6 class FrmHoneypot extends FrmValidate {
7 7
8 8 /**
9 + * Track the printed selectors so we do not print the same CSS twice.
10 + *
11 + * @since 6.22
12 + *
13 + * @var array
14 + */
15 + private static $printed_honeypot_selectors = array();
16 +
17 + /**
18 + * Option type.
19 + *
20 + * @since 6.21
21 + *
22 + * @var string
23 + */
24 + protected $option_type = 'global';
25 +
26 + /**
9 27 * @return string
10 28 */
11 29 protected function get_option_key() {
12 30 return 'honeypot';
@@ -14,11 +32,18 @@
14 32
15 33 /**
16 34 * @return bool
17 35 */
36 + private static function is_enabled() {
37 + return (bool) FrmAppHelper::get_settings()->honeypot;
38 + }
39 +
40 + /**
41 + * @return bool
42 + */
18 43 public function validate() {
19 44 if ( ! $this->is_option_on() || ! $this->check_honeypot_filter() ) {
20 - // never flag as honeypot spam if disabled.
45 + // Never flag as honeypot spam if disabled.
21 46 return true;
22 47 }
23 48 return ! $this->is_honeypot_spam();
24 49 }
@@ -23,21 +48,34 @@
23 48 return ! $this->is_honeypot_spam();
24 49 }
25 50
26 51 /**
27 - * @return boolean
52 + * @return bool
28 53 */
29 54 private function is_honeypot_spam() {
30 55 $is_honeypot_spam = $this->is_legacy_honeypot_spam();
56 +
31 57 if ( ! $is_honeypot_spam ) {
32 - // Check the newer honeypot input name which is randomly generated so it's more difficult to detect.
33 - $class_name = $this->get_honeypot_class_name();
34 - $honeypot_value = FrmAppHelper::get_param( $class_name, '', 'get', 'sanitize_text_field' );
35 - $is_honeypot_spam = '' !== $honeypot_value;
58 + $field_id = $this->get_honeypot_field_id();
59 +
60 + if ( ! $field_id ) {
61 + return false;
62 + }
63 +
64 + $value = $this->get_honeypot_field_value( $field_id );
65 + $is_honeypot_spam = '' !== $value;
36 66 }
37 67
38 - $form = $this->get_form();
39 - $atts = compact( 'form' );
68 + $atts = array(
69 + 'form' => $this->get_form(),
70 + );
71 +
72 + /**
73 + * Filters the honeypot spam check.
74 + *
75 + * @param bool $is_honeypot_spam Set to `true` if is spam.
76 + * @param array $atts Contains `form` and `fields`.
77 + */
40 78 return apply_filters( 'frm_process_honeypot', $is_honeypot_spam, $atts );
41 79 }
42 80
43 81 /**
@@ -58,29 +96,122 @@
58 96 return apply_filters( 'frm_run_honeypot', true, compact( 'form' ) );
59 97 }
60 98
61 99 /**
62 - * @return string
100 + * @param int $form_id Form ID.
101 + *
102 + * @return void
63 103 */
64 - private function check_honeypot_setting() {
65 - $form = $this->get_form();
66 - $key = $this->get_option_key();
67 - return $form->options[ $key ];
104 + public static function maybe_render_field( $form_id ) {
105 + $honeypot = new self( $form_id );
106 +
107 + if ( ! $honeypot->should_render_field() ) {
108 + return;
109 + }
110 +
111 + $max_field_id = FrmDb::get_var(
112 + 'frm_fields',
113 + array(),
114 + 'id',
115 + array(
116 + 'order_by' => 'id DESC',
117 + )
118 + );
119 +
120 + global $frm_vars;
121 + $offset = isset( $frm_vars['honeypot_selectors'] ) ? count( $frm_vars['honeypot_selectors'] ) + 1 : 1;
122 + $honeypot_field_id = $max_field_id ? $max_field_id + $offset : $offset;
123 + $class = class_exists( 'FrmProFormState' ) ? 'FrmProFormState' : 'FrmFormState';
124 + $class::set_initial_value( 'honeypot_field_id', $honeypot_field_id );
125 +
126 + $honeypot->render_field( $honeypot_field_id );
127 + self::maybe_print_honeypot_css();
68 128 }
69 129
70 130 /**
71 - * @param int $form_id
131 + * Maybe print honeypot JS.
72 132 *
133 + * @since 6.21
134 + *
73 135 * @return void
74 136 */
75 - public static function maybe_render_field( $form_id ) {
76 - $honeypot = new self( $form_id );
77 - if ( $honeypot->should_render_field() ) {
78 - $honeypot->render_field();
137 + public static function maybe_print_honeypot_js() {
138 + if ( FrmAppHelper::is_admin() || ! self::is_enabled() ) {
139 + return;
79 140 }
141 +
142 + $css = self::get_honeypot_field_css();
143 +
144 + if ( ! $css ) {
145 + return;
146 + }
147 +
148 + // There must be no empty lines inside the script. Otherwise, wpautop adds <p> tags which break script execution.
149 + printf(
150 + "<script>
151 + ( function() {
152 + const style = document.createElement( 'style' );
153 + style.appendChild( document.createTextNode( '%s' ) );
154 + document.head.appendChild( style );
155 + document.currentScript?.remove();
156 + } )();
157 + </script>",
158 + esc_js( $css )
159 + );
160 +
161 + global $frm_vars;
162 + self::$printed_honeypot_selectors = $frm_vars['honeypot_selectors'];
80 163 }
81 164
82 165 /**
166 + * Maybe print honeypot CSS in case JS doesn't run.
167 + *
168 + * @since 6.21
169 + *
170 + * @return void
171 + */
172 + public static function maybe_print_honeypot_css() {
173 + // Print the CSS if form is loaded by API.
174 + if ( ! FrmFormsHelper::form_is_loaded_by_api() ) {
175 + return;
176 + }
177 +
178 + $css = self::get_honeypot_field_css();
179 +
180 + if ( $css ) {
181 + echo '<style>' . esc_html( $css ) . '</style>';
182 + }
183 + }
184 +
185 + /**
186 + * Gets honeypot field CSS.
187 + *
188 + * @return string
189 + */
190 + private static function get_honeypot_field_css() {
191 + global $frm_vars;
192 +
193 + if ( empty( $frm_vars['honeypot_selectors'] ) ) {
194 + return '';
195 + }
196 +
197 + $selectors = $frm_vars['honeypot_selectors'];
198 +
199 + if ( self::$printed_honeypot_selectors ) {
200 + $selectors = array_diff( $selectors, self::$printed_honeypot_selectors );
201 +
202 + if ( ! $selectors ) {
203 + return '';
204 + }
205 + }
206 +
207 + return sprintf(
208 + '%s {visibility:hidden;overflow:hidden;width:0;height:0;position:absolute;}',
209 + implode( ',', $selectors )
210 + );
211 + }
212 +
213 + /**
83 214 * @return bool
84 215 */
85 216 public function should_render_field() {
86 217 return $this->is_option_on() && $this->check_honeypot_filter();
@@ -86,41 +217,108 @@
86 217 return $this->is_option_on() && $this->check_honeypot_filter();
87 218 }
88 219
89 220 /**
221 + * @param int $honeypot_field_id
222 + *
90 223 * @return void
91 224 */
92 - public function render_field() {
93 - $honeypot = $this->check_honeypot_setting();
94 - $form = $this->get_form();
95 - $class_name = $this->get_honeypot_class_name();
225 + public function render_field( $honeypot_field_id = 0 ) {
226 + if ( ! $honeypot_field_id ) {
227 + return;
228 + }
229 +
230 + $field_id = $honeypot_field_id;
231 + $field_key = $this->get_honeypot_field_key();
96 232 $input_attrs = array(
97 - 'id' => 'frm_email_' . absint( $form->id ),
98 - 'type' => 'strict' === $honeypot ? 'email' : 'text',
99 - 'class' => 'frm_verify',
100 - 'name' => $class_name,
101 - 'value' => FrmAppHelper::get_param( $class_name, '', 'get', 'wp_kses_post' ),
233 + 'id' => 'field_' . $field_key,
234 + 'type' => 'text',
235 + 'class' => 'frm_form_field form-field frm_verify',
236 + 'name' => 'item_meta[' . $field_id . ']',
237 + 'value' => $this->get_honeypot_field_value( $field_id ),
102 238 );
103 239
104 - if ( 'strict' !== $honeypot ) {
105 - $input_attrs['autocomplete'] = 'false';
106 - }
240 + $container_id = 'frm_field_' . $field_id . '_container';
241 + $this->track_html_id( $container_id );
242 + // phpcs:disable Generic.WhiteSpace.ScopeIndent
107 243 ?>
108 - <div class="<?php echo esc_attr( $class_name ); ?>" <?php echo in_array( $honeypot, array( true, 'strict' ), true ) ? '' : 'aria-hidden="true"'; ?>>
109 - <label for="frm_email_<?php echo esc_attr( $form->id ); ?>">
110 - <?php esc_html_e( 'If you are human, leave this field blank.', 'formidable' ); ?>
111 - </label>
112 - <input <?php FrmAppHelper::array_to_html_params( $input_attrs, true ); ?> <?php FrmFormsHelper::maybe_hide_inline(); ?> />
113 - </div>
244 + <div id="<?php echo esc_attr( $container_id ); ?>">
245 + <label for="<?php echo esc_attr( $input_attrs['id'] ); ?>" <?php FrmFormsHelper::maybe_hide_inline(); ?>>
246 + <?php esc_html_e( 'If you are human, leave this field blank.', 'formidable' ); ?>
247 + </label>
248 + <input <?php FrmAppHelper::array_to_html_params( $input_attrs, true ); ?> <?php FrmFormsHelper::maybe_hide_inline(); ?> />
249 + </div>
114 250 <?php
251 + // phpcs:enable Generic.WhiteSpace.ScopeIndent
115 252 }
116 253
117 254 /**
255 + * @param string $html_id
256 + *
257 + * @return void
258 + */
259 + private function track_html_id( $html_id ) {
260 + global $frm_vars;
261 +
262 + if ( ! isset( $frm_vars['honeypot_selectors'] ) ) {
263 + $frm_vars['honeypot_selectors'] = array();
264 + }
265 +
266 + $frm_vars['honeypot_selectors'][] = '#' . $html_id;
267 + }
268 +
269 + /**
270 + * @return int
271 + */
272 + private function get_honeypot_field_id() {
273 + $class = class_exists( 'FrmProFormState' ) ? 'FrmProFormState' : 'FrmFormState';
274 + return $class::get_from_request( 'honeypot_field_id', 0 );
275 + }
276 +
277 + /**
278 + * @return string
279 + */
280 + private function get_honeypot_field_key() {
281 + return FrmAppHelper::generate_new_key( 5 );
282 + }
283 +
284 + /**
285 + * Gets honeypot field value.
286 + *
287 + * @param string $field_id Field ID.
288 + *
289 + * @return string
290 + */
291 + private function get_honeypot_field_value( $field_id ) {
292 + $item_meta = FrmAppHelper::get_simple_request(
293 + array(
294 + 'param' => 'item_meta',
295 + 'default' => array(),
296 + 'type' => 'post',
297 + )
298 + );
299 +
300 + if ( ! $item_meta || ! is_array( $item_meta ) ) {
301 + return '';
302 + }
303 +
304 + return $item_meta[ $field_id ] ?? '';
305 + }
306 +
307 + /**
118 308 * Generate a random class name for our honeypot so it is less easy to detect.
119 309 *
120 310 * @return string The generated class name.
121 311 */
122 312 public static function generate_class_name() {
313 + $class_name = self::get_honeypot_class_name();
314 +
315 + if ( 'frm_verify' !== $class_name ) {
316 + // Re-use the option.
317 + // We can't generate a new class too often or the field may not be hidden.
318 + return $class_name;
319 + }
320 +
123 321 $prefix = 'frm__';
124 322 $class_name = $prefix . uniqid();
125 323 update_option( 'frm_honeypot_class', $class_name );
126 324 return $class_name;
@@ -128,13 +326,15 @@
128 326
129 327 /**
130 328 * @return string The current class name to use the for Honeypot field.
131 329 */
132 - private function get_honeypot_class_name() {
330 + private static function get_honeypot_class_name() {
133 331 $option = get_option( 'frm_honeypot_class' );
332 +
134 333 if ( ! is_string( $option ) ) {
135 334 // For backward compatibility use the old class name.
136 335 return 'frm_verify';
137 336 }
337 +
138 338 return $option;
139 339 }
140 340 }