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 +277 -25 6.4 → 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,19 +48,48 @@
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 - $honeypot_value = FrmAppHelper::get_param( 'frm_verify', '', 'get', 'sanitize_text_field' );
31 - $is_honeypot_spam = $honeypot_value !== '';
32 - $form = $this->get_form();
33 - $atts = compact( 'form' );
55 + $is_honeypot_spam = $this->is_legacy_honeypot_spam();
56 +
57 + if ( ! $is_honeypot_spam ) {
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;
66 + }
67 +
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 + */
34 78 return apply_filters( 'frm_process_honeypot', $is_honeypot_spam, $atts );
35 79 }
36 80
37 81 /**
82 + * Check the old frm_verify key. We'll continue to consider any entry with an frm_verify value as spam.
83 + *
84 + * @return bool
85 + */
86 + private function is_legacy_honeypot_spam() {
87 + $legacy_honeypot_value = FrmAppHelper::get_param( 'frm_verify', '', 'get', 'sanitize_text_field' );
88 + return '' !== $legacy_honeypot_value;
89 + }
90 +
91 + /**
38 92 * @return mixed either true, or false.
39 93 */
40 94 private function check_honeypot_filter() {
41 95 $form = $this->get_form();
@@ -42,29 +96,122 @@
42 96 return apply_filters( 'frm_run_honeypot', true, compact( 'form' ) );
43 97 }
44 98
45 99 /**
46 - * @return string
100 + * @param int $form_id Form ID.
101 + *
102 + * @return void
47 103 */
48 - private function check_honeypot_setting() {
49 - $form = $this->get_form();
50 - $key = $this->get_option_key();
51 - 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();
52 128 }
53 129
54 130 /**
55 - * @param int $form_id
131 + * Maybe print honeypot JS.
56 132 *
133 + * @since 6.21
134 + *
57 135 * @return void
58 136 */
59 - public static function maybe_render_field( $form_id ) {
60 - $honeypot = new self( $form_id );
61 - if ( $honeypot->should_render_field() ) {
62 - $honeypot->render_field();
137 + public static function maybe_print_honeypot_js() {
138 + if ( FrmAppHelper::is_admin() || ! self::is_enabled() ) {
139 + return;
63 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'];
64 163 }
65 164
66 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 + /**
67 214 * @return bool
68 215 */
69 216 public function should_render_field() {
70 217 return $this->is_option_on() && $this->check_honeypot_filter();
@@ -70,19 +217,124 @@
70 217 return $this->is_option_on() && $this->check_honeypot_filter();
71 218 }
72 219
73 220 /**
221 + * @param int $honeypot_field_id
222 + *
74 223 * @return void
75 224 */
76 - public function render_field() {
77 - $honeypot = $this->check_honeypot_setting();
78 - $form = $this->get_form();
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();
232 + $input_attrs = array(
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 ),
238 + );
239 +
240 + $container_id = 'frm_field_' . $field_id . '_container';
241 + $this->track_html_id( $container_id );
242 + // phpcs:disable Generic.WhiteSpace.ScopeIndent
79 243 ?>
80 - <div class="frm_verify" <?php echo in_array( $honeypot, array( true, 'strict' ), true ) ? '' : 'aria-hidden="true"'; ?>>
81 - <label for="frm_email_<?php echo esc_attr( $form->id ); ?>">
82 - <?php esc_html_e( 'If you are human, leave this field blank.', 'formidable' ); ?>
83 - </label>
84 - <input type="<?php echo esc_attr( 'strict' === $honeypot ? 'email' : 'text' ); ?>" class="frm_verify" id="frm_email_<?php echo esc_attr( $form->id ); ?>" name="frm_verify" value="<?php echo esc_attr( FrmAppHelper::get_param( 'frm_verify', '', 'get', 'wp_kses_post' ) ); ?>" <?php FrmFormsHelper::maybe_hide_inline(); ?> />
85 - </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>
86 250 <?php
251 + // phpcs:enable Generic.WhiteSpace.ScopeIndent
252 + }
253 +
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 + /**
308 + * Generate a random class name for our honeypot so it is less easy to detect.
309 + *
310 + * @return string The generated class name.
311 + */
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 +
321 + $prefix = 'frm__';
322 + $class_name = $prefix . uniqid();
323 + update_option( 'frm_honeypot_class', $class_name );
324 + return $class_name;
325 + }
326 +
327 + /**
328 + * @return string The current class name to use the for Honeypot field.
329 + */
330 + private static function get_honeypot_class_name() {
331 + $option = get_option( 'frm_honeypot_class' );
332 +
333 + if ( ! is_string( $option ) ) {
334 + // For backward compatibility use the old class name.
335 + return 'frm_verify';
336 + }
337 +
338 + return $option;
87 339 }
88 340 }