Formidable Forms – Form Builder for WordPress - Version 4.11

Version Description

  • New: Added a quick and easy Name field with options for First, Middle, and Last names.
  • New: Added a more powerful spam protection using JavaScript. This can be turned on in the settings for each form.
  • New: Added Honeypot options to form settings and changed the default Honeypot behaviour to avoid the false positives some people are seeing on mobile devices.
  • New: Added a frm_process_honeypot filter for gracefully handling honeypot spam.
  • Fix: A warning was getting logged when exporting a form as XML.
Download this release

Release Info

Developer formidableforms
Plugin Icon 128x128 Formidable Forms – Form Builder for WordPress
Version 4.11
Comparing to
See all releases

Code changes from version 4.10.03 to 4.11

classes/controllers/FrmFormsController.php CHANGED
@@ -136,13 +136,29 @@ class FrmFormsController {
136
 
137
  do_action( 'frm_before_update_form_settings', $id );
138
 
 
 
139
  FrmForm::update( $id, $_POST );
140
 
 
 
 
 
 
141
  $message = __( 'Settings Successfully Updated', 'formidable' );
142
 
143
  return self::get_settings_vars( $id, array(), compact( 'message', 'warnings' ) );
144
  }
145
 
 
 
 
 
 
 
 
 
 
146
  public static function update( $values = array() ) {
147
  if ( empty( $values ) ) {
148
  $values = $_POST;
@@ -1116,7 +1132,18 @@ class FrmFormsController {
1116
  public static function advanced_settings( $values ) {
1117
  $first_h3 = 'frm_first_h3';
1118
 
1119
- include( FrmAppHelper::plugin_path() . '/classes/views/frm-forms/settings-advanced.php' );
 
 
 
 
 
 
 
 
 
 
 
1120
  }
1121
 
1122
  /**
136
 
137
  do_action( 'frm_before_update_form_settings', $id );
138
 
139
+ $antispam_was_on = self::antispam_was_on( $id );
140
+
141
  FrmForm::update( $id, $_POST );
142
 
143
+ $antispam_is_on = ! empty( $_POST['options']['antispam'] );
144
+ if ( $antispam_is_on !== $antispam_was_on ) {
145
+ FrmAntiSpam::clear_caches();
146
+ }
147
+
148
  $message = __( 'Settings Successfully Updated', 'formidable' );
149
 
150
  return self::get_settings_vars( $id, array(), compact( 'message', 'warnings' ) );
151
  }
152
 
153
+ /**
154
+ * @param int $form_id
155
+ * @return bool
156
+ */
157
+ private static function antispam_was_on( $form_id ) {
158
+ $form = FrmForm::getOne( $form_id );
159
+ return ! empty( $form->options['antispam'] );
160
+ }
161
+
162
  public static function update( $values = array() ) {
163
  if ( empty( $values ) ) {
164
  $values = $_POST;
1132
  public static function advanced_settings( $values ) {
1133
  $first_h3 = 'frm_first_h3';
1134
 
1135
+ include FrmAppHelper::plugin_path() . '/classes/views/frm-forms/settings-advanced.php';
1136
+ }
1137
+
1138
+ /**
1139
+ * @param array $values
1140
+ */
1141
+ private static function render_spam_settings( $values ) {
1142
+ if ( function_exists( 'akismet_http_post' ) ) {
1143
+ include FrmAppHelper::plugin_path() . '/classes/views/frm-forms/spam-settings/akismet.php';
1144
+ }
1145
+ include FrmAppHelper::plugin_path() . '/classes/views/frm-forms/spam-settings/honeypot.php';
1146
+ include FrmAppHelper::plugin_path() . '/classes/views/frm-forms/spam-settings/antispam.php';
1147
  }
1148
 
1149
  /**
classes/controllers/FrmHooksController.php CHANGED
@@ -84,6 +84,12 @@ class FrmHooksController {
84
 
85
  add_filter( 'cron_schedules', 'FrmUsageController::add_schedules' );
86
  add_action( 'formidable_send_usage', 'FrmUsageController::send_snapshot' );
 
 
 
 
 
 
87
  }
88
 
89
  public static function load_admin_hooks() {
84
 
85
  add_filter( 'cron_schedules', 'FrmUsageController::add_schedules' );
86
  add_action( 'formidable_send_usage', 'FrmUsageController::send_snapshot' );
87
+
88
+ /**
89
+ * Make name field work with View.
90
+ * FrmProContent::replace_single_shortcode() applies this filter like 'frm_keep_' . $field->type . '_value_array'
91
+ */
92
+ add_filter( 'frm_keep_name_value_array', '__return_true' );
93
  }
94
 
95
  public static function load_admin_hooks() {
classes/factories/FrmFieldFactory.php CHANGED
@@ -99,6 +99,7 @@ class FrmFieldFactory {
99
  'html' => 'FrmFieldHTML',
100
  'hidden' => 'FrmFieldHidden',
101
  'captcha' => 'FrmFieldCaptcha',
 
102
  );
103
 
104
  $class = isset( $type_classes[ $field_type ] ) ? $type_classes[ $field_type ] : '';
99
  'html' => 'FrmFieldHTML',
100
  'hidden' => 'FrmFieldHidden',
101
  'captcha' => 'FrmFieldCaptcha',
102
+ 'name' => 'FrmFieldName',
103
  );
104
 
105
  $class = isset( $type_classes[ $field_type ] ) ? $type_classes[ $field_type ] : '';
classes/helpers/FrmAppHelper.php CHANGED
@@ -11,7 +11,7 @@ class FrmAppHelper {
11
  /**
12
  * @since 2.0
13
  */
14
- public static $plug_version = '4.10.03';
15
 
16
  /**
17
  * @since 1.07.02
11
  /**
12
  * @since 2.0
13
  */
14
+ public static $plug_version = '4.11';
15
 
16
  /**
17
  * @since 1.07.02
classes/helpers/FrmCSVExportHelper.php CHANGED
@@ -121,6 +121,11 @@ class FrmCSVExportHelper {
121
  }
122
 
123
  private static function field_headings( $col ) {
 
 
 
 
 
124
  $field_headings = array();
125
  $separate_values = array( 'user_id', 'file', 'data', 'date' );
126
  if ( isset( $col->field_options['separate_value'] ) && $col->field_options['separate_value'] && ! in_array( $col->type, $separate_values, true ) ) {
@@ -154,9 +159,11 @@ class FrmCSVExportHelper {
154
  }
155
 
156
  $fields_by_repeater_id[ $repeater_id ][] = $col;
157
- } else {
158
- $headings += self::field_headings( $col );
159
  }
 
 
160
  }
161
  unset( $repeater_id, $col );
162
 
@@ -413,6 +420,18 @@ class FrmCSVExportHelper {
413
  private static function add_array_values_to_columns( &$row, $atts ) {
414
  if ( is_array( $atts['field_value'] ) ) {
415
  foreach ( $atts['field_value'] as $key => $sub_value ) {
 
 
 
 
 
 
 
 
 
 
 
 
416
  $column_key = $atts['col']->id . '_' . $key;
417
  if ( ! is_numeric( $key ) && isset( self::$headings[ $column_key ] ) ) {
418
  $row[ $column_key ] = $sub_value;
121
  }
122
 
123
  private static function field_headings( $col ) {
124
+ $field_type_obj = FrmFieldFactory::get_field_factory( $col );
125
+ if ( ! empty( $field_type_obj->is_combo_field ) ) { // This is combo field.
126
+ return $field_type_obj->get_export_headings();
127
+ }
128
+
129
  $field_headings = array();
130
  $separate_values = array( 'user_id', 'file', 'data', 'date' );
131
  if ( isset( $col->field_options['separate_value'] ) && $col->field_options['separate_value'] && ! in_array( $col->type, $separate_values, true ) ) {
159
  }
160
 
161
  $fields_by_repeater_id[ $repeater_id ][] = $col;
162
+
163
+ continue;
164
  }
165
+
166
+ $headings += self::field_headings( $col );
167
  }
168
  unset( $repeater_id, $col );
169
 
420
  private static function add_array_values_to_columns( &$row, $atts ) {
421
  if ( is_array( $atts['field_value'] ) ) {
422
  foreach ( $atts['field_value'] as $key => $sub_value ) {
423
+ if ( is_array( $sub_value ) ) {
424
+ // This is combo field inside repeater. The heading key has this format: [86_first[0]].
425
+ foreach ( $sub_value as $sub_key => $sub_sub_value ) {
426
+ $column_key = $atts['col']->id . '_' . $sub_key . '[' . $key . ']';
427
+ if ( ! is_numeric( $sub_key ) && isset( self::$headings[ $column_key ] ) ) {
428
+ $row[ $column_key ] = $sub_sub_value;
429
+ }
430
+ }
431
+
432
+ continue;
433
+ }
434
+
435
  $column_key = $atts['col']->id . '_' . $key;
436
  if ( ! is_numeric( $key ) && isset( self::$headings[ $column_key ] ) ) {
437
  $row[ $column_key ] = $sub_value;
classes/helpers/FrmFormsHelper.php CHANGED
@@ -365,6 +365,8 @@ class FrmFormsHelper {
365
  'success_msg' => $frm_settings->success_msg,
366
  'show_form' => 0,
367
  'akismet' => '',
 
 
368
  'no_save' => 0,
369
  'ajax_load' => 0,
370
  'js_validate' => 0,
365
  'success_msg' => $frm_settings->success_msg,
366
  'show_form' => 0,
367
  'akismet' => '',
368
+ 'honeypot' => 'basic',
369
+ 'antispam' => 0,
370
  'no_save' => 0,
371
  'ajax_load' => 0,
372
  'js_validate' => 0,
classes/helpers/FrmXMLHelper.php CHANGED
@@ -1202,15 +1202,11 @@ class FrmXMLHelper {
1202
  * @since 3.06
1203
  */
1204
  private static function remove_defaults( $defaults, &$saved ) {
1205
- $array_defaults = array_filter( $defaults, 'is_array' );
1206
- foreach ( $array_defaults as $d => $default ) {
1207
- // compare array defaults
1208
- if ( $default == $saved[ $d ] ) {
1209
- unset( $saved[ $d ] );
1210
  }
1211
- unset( $defaults[ $d ] );
1212
  }
1213
- $saved = array_diff_assoc( (array) $saved, $defaults );
1214
  }
1215
 
1216
  /**
1202
  * @since 3.06
1203
  */
1204
  private static function remove_defaults( $defaults, &$saved ) {
1205
+ foreach ( $saved as $key => $value ) {
1206
+ if ( isset( $defaults[ $key ] ) && $defaults[ $key ] === $value ) {
1207
+ unset( $saved[ $key ] );
 
 
1208
  }
 
1209
  }
 
1210
  }
1211
 
1212
  /**
classes/models/FrmAntiSpam.php ADDED
@@ -0,0 +1,326 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( ! defined( 'ABSPATH' ) ) {
3
+ die( 'You are not allowed to call this page directly.' );
4
+ }
5
+
6
+ /**
7
+ * Class FrmAntiSpam.
8
+ *
9
+ * This token class generates tokens that are used in our Anti-Spam checking.
10
+ *
11
+ * @since xx.xx
12
+ */
13
+ class FrmAntiSpam extends FrmValidate {
14
+
15
+ /**
16
+ * @return string
17
+ */
18
+ protected function get_option_key() {
19
+ return 'antispam';
20
+ }
21
+
22
+ /**
23
+ * @param int $form_id
24
+ */
25
+ public static function maybe_init( $form_id ) {
26
+ $antispam = new self( $form_id );
27
+ if ( $antispam->run_antispam() ) {
28
+ $antispam->init();
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Initialise the actions for the Anti-spam.
34
+ *
35
+ * @since xx.xx
36
+ */
37
+ public function init() {
38
+ add_filter( 'frm_form_attributes', array( $this, 'add_token_to_form' ), 10, 1 );
39
+ add_filter( 'frm_form_div_attributes', array( $this, 'add_token_to_form' ), 10, 1 );
40
+ }
41
+
42
+ /**
43
+ * Return a valid token.
44
+ *
45
+ * @since xx.xx
46
+ *
47
+ * @param mixed $current True to use current time, otherwise a timestamp string.
48
+ *
49
+ * @return string Token.
50
+ */
51
+ private function get( $current = true ) {
52
+ // If $current was not passed, or it is true, we use the current timestamp.
53
+ // If $current was passed in as a string, we'll use that passed in timestamp.
54
+ if ( $current !== true ) {
55
+ $time = $current;
56
+ } else {
57
+ $time = time();
58
+ }
59
+
60
+ // Format the timestamp to be less exact, as we want to deal in days.
61
+ // June 19th, 2020 would get formatted as: 1906202017125.
62
+ // Day of the month, month number, year, day number of the year, week number of the year.
63
+ $token_date = gmdate( 'dmYzW', $time );
64
+
65
+ // Combine our token date and our token salt, and md5 it.
66
+ $form_token_string = md5( $token_date . $this->get_antispam_secret_key() );
67
+
68
+ return $form_token_string;
69
+ }
70
+
71
+ private function get_antispam_secret_key() {
72
+ $secret_key = get_option( 'frm_antispam_secret_key' );
73
+
74
+ // If we already have the secret, send it back.
75
+ if ( false !== $secret_key ) {
76
+ return base64_decode( $secret_key ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
77
+ }
78
+
79
+ // We don't have a secret, so let's generate one.
80
+ $secret_key = is_callable( 'sodium_crypto_secretbox_keygen' ) ? sodium_crypto_secretbox_keygen() : wp_generate_password( 32, true, true );
81
+ add_option( 'frm_antispam_secret_key', base64_encode( $secret_key ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
82
+
83
+ return $secret_key;
84
+ }
85
+
86
+ /**
87
+ * Generate the array of valid tokens to check for. These include two days
88
+ * before the current date to account for long cache times.
89
+ *
90
+ * These two filters are available if a user wants to extend the times.
91
+ * 'frm_form_token_check_before_today'
92
+ * 'frm_form_token_check_after_today'
93
+ *
94
+ * @since xx.xx
95
+ *
96
+ * @return array Array of all valid tokens to check against.
97
+ */
98
+ private function get_valid_tokens() {
99
+ $current_date = time();
100
+
101
+ // Create our array of times to check before today. A user with a longer
102
+ // cache time can extend this. A user with a shorter cache time can remove times.
103
+ $valid_token_times_before = apply_filters(
104
+ 'frm_form_token_check_before_today',
105
+ array(
106
+ ( 2 * DAY_IN_SECONDS ), // Two days ago.
107
+ ( 1 * DAY_IN_SECONDS ), // One day ago.
108
+ )
109
+ );
110
+
111
+ // Mostly to catch edge cases like the form page loading and submitting on two different days.
112
+ // This probably won't be filtered by users too much, but they could extend it.
113
+ $valid_token_times_after = apply_filters(
114
+ 'frm_form_token_check_after_today',
115
+ array(
116
+ ( 45 * MINUTE_IN_SECONDS ), // Add in 45 minutes past today to catch some midnight edge cases.
117
+ )
118
+ );
119
+
120
+ // Built up our valid tokens.
121
+ $valid_tokens = array();
122
+
123
+ // Add in all the previous times we check.
124
+ foreach ( $valid_token_times_before as $time ) {
125
+ $valid_tokens[] = $this->get( $current_date - $time );
126
+ }
127
+
128
+ // Add in our current date.
129
+ $valid_tokens[] = $this->get( $current_date );
130
+
131
+ // Add in the times after our check.
132
+ foreach ( $valid_token_times_after as $time ) {
133
+ $valid_tokens[] = $this->get( $current_date + $time );
134
+ }
135
+
136
+ return $valid_tokens;
137
+ }
138
+
139
+ /**
140
+ * Check if the given token is valid or not.
141
+ *
142
+ * Tokens are valid for some period of time (see frm_token_validity_in_hours
143
+ * and frm_token_validity_in_days to extend the validation period).
144
+ * By default tokens are valid for day.
145
+ *
146
+ * @since xx.xx
147
+ *
148
+ * @param string $token Token to validate.
149
+ *
150
+ * @return bool Whether the token is valid or not.
151
+ */
152
+ private function verify( $token ) {
153
+ // Check to see if our token is inside of the valid tokens.
154
+ return in_array( $token, $this->get_valid_tokens(), true );
155
+ }
156
+
157
+ /**
158
+ * Add the token field to the form.
159
+ *
160
+ * @since xx.xx
161
+ *
162
+ * @param string $attributes
163
+ *
164
+ * @return string
165
+ */
166
+ public function add_token_to_form( $attributes ) {
167
+ $attributes .= ' data-token="' . esc_attr( $this->get() ) . '"';
168
+ return $attributes;
169
+ }
170
+
171
+ /**
172
+ * @param int $form_id
173
+ */
174
+ public static function maybe_echo_token( $form_id ) {
175
+ $antispam = new self( $form_id );
176
+ if ( $antispam->run_antispam() ) {
177
+ echo 'data-token="' . esc_attr( $antispam->get() ) . '"';
178
+ }
179
+ }
180
+
181
+ /**
182
+ * @return bool
183
+ */
184
+ public function run_antispam() {
185
+ return $this->is_option_on() && apply_filters( 'frm_run_antispam', true, $this->form_id );
186
+ }
187
+
188
+ /**
189
+ * Validate Anti-spam if enabled.
190
+ *
191
+ * @since xx.xx
192
+ *
193
+ * @return bool|string True or a string with the error.
194
+ */
195
+ public function validate() {
196
+ if ( ! $this->run_antispam() ) {
197
+ return true;
198
+ }
199
+
200
+ $token = FrmAppHelper::get_param( 'antispam_token', '', 'post', 'sanitize_text_field' );
201
+
202
+ // If the antispam setting is enabled and we don't have a token, bail.
203
+ if ( ! $token ) {
204
+ if ( FrmAppHelper::is_admin_page( 'formidable-entries' ) ) {
205
+ // add an exception for the entries page.
206
+ return true;
207
+ }
208
+ return $this->process_antispam_filter( $this->get_missing_token_message() );
209
+ }
210
+
211
+ // Verify the token.
212
+ if ( ! $this->verify( $token ) ) {
213
+ return $this->process_antispam_filter( $this->get_invalid_token_message() );
214
+ }
215
+
216
+ return $this->process_antispam_filter( true );
217
+ }
218
+
219
+ /**
220
+ * @return bool True if saving a draft.
221
+ */
222
+ private function is_saving_a_draft() {
223
+ global $frm_vars;
224
+ if ( empty( $frm_vars['form_params'] ) ) {
225
+ return false;
226
+ }
227
+ $form_params = $frm_vars['form_params'];
228
+ if ( ! isset( $form_params[ $this->form_id ] ) ) {
229
+ return false;
230
+ }
231
+ $this_form_params = $form_params[ $this->form_id ];
232
+ return ! empty( $this_form_params['action'] ) && 'update' === $this_form_params['action'];
233
+ }
234
+
235
+ /**
236
+ * Helper to run our filter on all the responses for the antispam checks.
237
+ *
238
+ * @since xx.xx
239
+ *
240
+ * @param bool|string $is_valid Is valid entry or not.
241
+ *
242
+ * @return bool|string Is valid or message.
243
+ */
244
+ private function process_antispam_filter( $is_valid ) {
245
+ return apply_filters( 'frm_process_antispam', $is_valid );
246
+ }
247
+
248
+ /**
249
+ * Helper to get the missing token message.
250
+ *
251
+ * @since xx.xx
252
+ *
253
+ * @return string missing token message.
254
+ */
255
+ private function get_missing_token_message() {
256
+ return esc_html__( 'This page isn\'t loading JavaScript properly, and the form will not be able to submit.', 'formidable' ) . $this->maybe_get_support_text();
257
+ }
258
+
259
+ /**
260
+ * Helper to get the invalid token message.
261
+ *
262
+ * @since xx.xx
263
+ *
264
+ * @return string Invalid token message.
265
+ */
266
+ private function get_invalid_token_message() {
267
+ return esc_html__( 'Form token is invalid. Please refresh the page.', 'formidable' ) . $this->maybe_get_support_text();
268
+ }
269
+
270
+ /**
271
+ * If a user is a super admin, add a support link to the message.
272
+ *
273
+ * @since xx.xx
274
+ *
275
+ * @return string Support text if super admin, empty string if not.
276
+ */
277
+ private function maybe_get_support_text() {
278
+ // If user isn't a super admin, don't return any text.
279
+ if ( ! is_super_admin() ) {
280
+ return '';
281
+ }
282
+
283
+ // If the user is an admin, return text with a link to support.
284
+ // We add a space here to seperate the sentences, but outside of the localized
285
+ // text to avoid it being removed.
286
+ return ' ' . sprintf(
287
+ // translators: %1$s start link, %2$s end link.
288
+ esc_html__( 'Please check out our %1$stroubleshooting guide%2$s for details on resolving this issue.', 'formidable' ),
289
+ '<a href="https://formidableforms.com/knowledgebase/add-spam-protection/">',
290
+ '</a>'
291
+ );
292
+ }
293
+
294
+ /**
295
+ * Clear third party cache plugins to avoid data-tokens missing or appearing when the antispam setting is changed.
296
+ */
297
+ public static function clear_caches() {
298
+ self::clear_w3_total_cache();
299
+ self::clear_wp_fastest_cache();
300
+ self::clear_wp_super_cache();
301
+ self::clear_wp_optimize();
302
+ }
303
+
304
+ private static function clear_w3_total_cache() {
305
+ if ( is_callable( 'w3tc_flush_all' ) ) {
306
+ w3tc_flush_all();
307
+ }
308
+ }
309
+
310
+ private static function clear_wp_fastest_cache() {
311
+ do_action( 'wpfc_clear_all_cache' );
312
+ }
313
+
314
+ private static function clear_wp_super_cache() {
315
+ if ( function_exists( 'wp_cache_clean_cache' ) ) {
316
+ global $file_prefix;
317
+ wp_cache_clean_cache( $file_prefix, true );
318
+ }
319
+ }
320
+
321
+ private static function clear_wp_optimize() {
322
+ if ( class_exists( 'WP_Optimize' ) ) {
323
+ WP_Optimize()->get_page_cache()->purge();
324
+ }
325
+ }
326
+ }
classes/models/FrmEntryValidate.php CHANGED
@@ -229,43 +229,63 @@ class FrmEntryValidate {
229
  return;
230
  }
231
 
232
- if ( self::is_honeypot_spam() || self::is_spam_bot() ) {
 
 
 
233
  $errors['spam'] = __( 'Your entry appears to be spam!', 'formidable' );
234
- }
235
-
236
- if ( self::blacklist_check( $values ) ) {
237
  $errors['spam'] = __( 'Your entry appears to be blocked spam!', 'formidable' );
238
- }
239
-
240
- if ( self::is_akismet_spam( $values ) ) {
241
- if ( self::is_akismet_enabled_for_user( $values['form_id'] ) ) {
242
- $errors['spam'] = __( 'Your entry appears to be spam!', 'formidable' );
243
- }
244
  }
245
  }
246
 
247
- private static function is_honeypot_spam() {
248
- $honeypot_value = FrmAppHelper::get_param( 'frm_verify', '', 'get', 'sanitize_text_field' );
 
 
 
 
 
 
249
 
250
- return ( $honeypot_value !== '' );
 
 
 
 
 
 
251
  }
252
 
 
 
 
253
  private static function is_spam_bot() {
254
  $ip = FrmAppHelper::get_ip_address();
255
 
256
  return empty( $ip );
257
  }
258
 
 
 
 
 
259
  private static function is_akismet_spam( $values ) {
260
  global $wpcom_api_key;
261
 
262
  return ( is_callable( 'Akismet::http_post' ) && ( get_option( 'wordpress_api_key' ) || $wpcom_api_key ) && self::akismet( $values ) );
263
  }
264
 
 
 
 
 
265
  private static function is_akismet_enabled_for_user( $form_id ) {
266
  $form = FrmForm::getOne( $form_id );
267
 
268
- return ( isset( $form->options['akismet'] ) && ! empty( $form->options['akismet'] ) && ( $form->options['akismet'] != 'logged' || ! is_user_logged_in() ) );
269
  }
270
 
271
  public static function blacklist_check( $values ) {
229
  return;
230
  }
231
 
232
+ $antispam_check = self::is_antispam_check( $values['form_id'] );
233
+ if ( is_string( $antispam_check ) ) {
234
+ $errors['spam'] = $antispam_check;
235
+ } elseif ( self::is_honeypot_spam( $values ) || self::is_spam_bot() ) {
236
  $errors['spam'] = __( 'Your entry appears to be spam!', 'formidable' );
237
+ } elseif ( self::blacklist_check( $values ) ) {
 
 
238
  $errors['spam'] = __( 'Your entry appears to be blocked spam!', 'formidable' );
239
+ } elseif ( self::is_akismet_spam( $values ) && self::is_akismet_enabled_for_user( $values['form_id'] ) ) {
240
+ $errors['spam'] = __( 'Your entry appears to be spam!', 'formidable' );
 
 
 
 
241
  }
242
  }
243
 
244
+ /**
245
+ * @param int $form_id
246
+ * @return boolean
247
+ */
248
+ private static function is_antispam_check( $form_id ) {
249
+ $aspm = new FrmAntiSpam( $form_id );
250
+ return $aspm->validate();
251
+ }
252
 
253
+ /**
254
+ * @param array $values
255
+ * @return boolean
256
+ */
257
+ private static function is_honeypot_spam( $values ) {
258
+ $honeypot = new FrmHoneypot( $values['form_id'] );
259
+ return ! $honeypot->validate();
260
  }
261
 
262
+ /**
263
+ * @return boolean
264
+ */
265
  private static function is_spam_bot() {
266
  $ip = FrmAppHelper::get_ip_address();
267
 
268
  return empty( $ip );
269
  }
270
 
271
+ /**
272
+ * @param array $values
273
+ * @return boolean
274
+ */
275
  private static function is_akismet_spam( $values ) {
276
  global $wpcom_api_key;
277
 
278
  return ( is_callable( 'Akismet::http_post' ) && ( get_option( 'wordpress_api_key' ) || $wpcom_api_key ) && self::akismet( $values ) );
279
  }
280
 
281
+ /**
282
+ * @param int $form_id
283
+ * @return bool
284
+ */
285
  private static function is_akismet_enabled_for_user( $form_id ) {
286
  $form = FrmForm::getOne( $form_id );
287
 
288
+ return ( isset( $form->options['akismet'] ) && ! empty( $form->options['akismet'] ) && ( $form->options['akismet'] !== 'logged' || ! is_user_logged_in() ) );
289
  }
290
 
291
  public static function blacklist_check( $values ) {
classes/models/FrmField.php CHANGED
@@ -42,6 +42,10 @@ class FrmField {
42
  'name' => __( 'Number', 'formidable' ),
43
  'icon' => 'frm_icon_font frm_hashtag_icon',
44
  ),
 
 
 
 
45
  'phone' => array(
46
  'name' => __( 'Phone', 'formidable' ),
47
  'icon' => 'frm_icon_font frm_phone_icon',
@@ -1069,4 +1073,18 @@ class FrmField {
1069
  */
1070
  return apply_filters( 'frm_is_field_type', $is_field_type, compact( 'field', 'is_type' ) );
1071
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1072
  }
42
  'name' => __( 'Number', 'formidable' ),
43
  'icon' => 'frm_icon_font frm_hashtag_icon',
44
  ),
45
+ 'name' => array(
46
+ 'name' => __( 'Name', 'formidable' ),
47
+ 'icon' => 'frm_icon_font frm_user_name_icon',
48
+ ),
49
  'phone' => array(
50
  'name' => __( 'Phone', 'formidable' ),
51
  'icon' => 'frm_icon_font frm_phone_icon',
1073
  */
1074
  return apply_filters( 'frm_is_field_type', $is_field_type, compact( 'field', 'is_type' ) );
1075
  }
1076
+
1077
+ /**
1078
+ * Checks if the given field array is a combo field.
1079
+ *
1080
+ * @since 4.10.02
1081
+ *
1082
+ * @param array $field Field array.
1083
+ * @return bool
1084
+ */
1085
+ public static function is_combo_field( $field ) {
1086
+ $field_type_obj = FrmFieldFactory::get_field_factory( $field );
1087
+
1088
+ return ! empty( $field_type_obj->is_combo_field );
1089
+ }
1090
  }
classes/models/FrmHoneypot.php ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( ! defined( 'ABSPATH' ) ) {
3
+ die( 'You are not allowed to call this page directly.' );
4
+ }
5
+
6
+ class FrmHoneypot extends FrmValidate {
7
+
8
+ /**
9
+ * @return string
10
+ */
11
+ protected function get_option_key() {
12
+ return 'honeypot';
13
+ }
14
+
15
+ /**
16
+ * @return bool
17
+ */
18
+ public function validate() {
19
+ if ( ! $this->is_option_on() || ! $this->check_honeypot_filter() ) {
20
+ // never flag as honeypot spam if disabled.
21
+ return true;
22
+ }
23
+ return ! $this->is_honeypot_spam();
24
+ }
25
+
26
+ /**
27
+ * @return boolean
28
+ */
29
+ 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' );
34
+ return apply_filters( 'frm_process_honeypot', $is_honeypot_spam, $atts );
35
+ }
36
+
37
+ /**
38
+ * @return mixed either true, or false.
39
+ */
40
+ private function check_honeypot_filter() {
41
+ $form = $this->get_form();
42
+ return apply_filters( 'frm_run_honeypot', true, compact( 'form' ) );
43
+ }
44
+
45
+ /**
46
+ * @return string
47
+ */
48
+ private function check_honeypot_setting() {
49
+ $form = $this->get_form();
50
+ $key = $this->get_option_key();
51
+ return $form->options[ $key ];
52
+ }
53
+
54
+ /**
55
+ * @param int $form_id
56
+ */
57
+ public static function maybe_render_field( $form_id ) {
58
+ $honeypot = new self( $form_id );
59
+ if ( $honeypot->should_render_field() ) {
60
+ $honeypot->render_field();
61
+ }
62
+ }
63
+
64
+ /**
65
+ * @return bool
66
+ */
67
+ public function should_render_field() {
68
+ return $this->is_option_on() && $this->check_honeypot_filter();
69
+ }
70
+
71
+ public function render_field() {
72
+ $honeypot = $this->check_honeypot_setting();
73
+ $form = $this->get_form();
74
+ ?>
75
+ <div class="frm_verify" <?php echo in_array( $honeypot, array( true, 'strict' ), true ) ? '' : 'aria-hidden="true"'; ?>>
76
+ <label for="frm_email_<?php echo esc_attr( $form->id ); ?>">
77
+ <?php esc_html_e( 'If you are human, leave this field blank.', 'formidable' ); ?>
78
+ </label>
79
+ <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(); ?> />
80
+ </div>
81
+ <?php
82
+ }
83
+ }
classes/models/FrmUsage.php CHANGED
@@ -215,6 +215,8 @@ class FrmUsage {
215
  $settings = array(
216
  'form_class',
217
  'akismet',
 
 
218
  'custom_style',
219
  'success_action',
220
  'show_form',
215
  $settings = array(
216
  'form_class',
217
  'akismet',
218
+ 'honeypot',
219
+ 'antispam',
220
  'custom_style',
221
  'success_action',
222
  'show_form',
classes/models/FrmValidate.php ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( ! defined( 'ABSPATH' ) ) {
3
+ die( 'You are not allowed to call this page directly.' );
4
+ }
5
+
6
+ abstract class FrmValidate {
7
+
8
+ /**
9
+ * @var int $form_id
10
+ */
11
+ protected $form_id;
12
+
13
+ /**
14
+ * @var object $form
15
+ */
16
+ protected $form;
17
+
18
+ /**
19
+ * @param int $form_id
20
+ */
21
+ public function __construct( $form_id ) {
22
+ $this->form_id = $form_id;
23
+ }
24
+
25
+ /**
26
+ * @return object $form
27
+ */
28
+ protected function get_form() {
29
+ if ( ! isset( $this->form ) ) {
30
+ $this->form = FrmForm::getOne( $this->form_id );
31
+ }
32
+ return $this->form;
33
+ }
34
+
35
+ /**
36
+ * @return bool
37
+ */
38
+ protected function is_option_on() {
39
+ $form = $this->get_form();
40
+ $key = $this->get_option_key();
41
+ return ! empty( $form->options[ $key ] ) && 'off' !== $form->options[ $key ];
42
+ }
43
+
44
+ /**
45
+ * @return bool
46
+ */
47
+ abstract public function validate();
48
+
49
+ /**
50
+ * Track the form option key used for is_option_on function.
51
+ *
52
+ * @return string
53
+ */
54
+ abstract protected function get_option_key();
55
+ }
classes/models/fields/FrmFieldCombo.php ADDED
@@ -0,0 +1,466 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * Combo field - Field contains sub fields
4
+ *
5
+ * @package Formidable
6
+ * @since 4.11
7
+ */
8
+
9
+ if ( ! defined( 'ABSPATH' ) ) {
10
+ die( 'You are not allowed to call this page directly.' );
11
+ }
12
+
13
+ class FrmFieldCombo extends FrmFieldType {
14
+
15
+ /**
16
+ * Does the html for this field label need to include "for"?
17
+ *
18
+ * @var bool
19
+ * @since 3.0
20
+ */
21
+ protected $has_for_label = false;
22
+
23
+ /**
24
+ * Sub fields.
25
+ *
26
+ * @var array
27
+ */
28
+ protected $sub_fields = array();
29
+
30
+ /**
31
+ * This is used to check if field is combo field.
32
+ *
33
+ * @var bool
34
+ */
35
+ public $is_combo_field = true;
36
+
37
+ /**
38
+ * Gets ALL subfields.
39
+ *
40
+ * @return array
41
+ */
42
+ public function get_sub_fields() {
43
+ return $this->sub_fields;
44
+ }
45
+
46
+ /**
47
+ * Registers sub fields.
48
+ *
49
+ * @param array $sub_fields Sub fields. Accepts array or array or array of string.
50
+ */
51
+ protected function register_sub_fields( array $sub_fields ) {
52
+ $defaults = $this->get_default_sub_field();
53
+
54
+ foreach ( $sub_fields as $name => $sub_field ) {
55
+ if ( empty( $sub_field ) ) {
56
+ continue;
57
+ }
58
+
59
+ if ( is_array( $sub_field ) ) {
60
+ $sub_field = wp_parse_args( $sub_field, $defaults );
61
+ $sub_field['name'] = $name;
62
+ $this->sub_fields[ $name ] = $sub_field;
63
+ continue;
64
+ }
65
+
66
+ if ( is_string( $sub_field ) ) {
67
+ $this->sub_fields[ $name ] = wp_parse_args(
68
+ array(
69
+ 'name' => $name,
70
+ 'label' => $sub_field,
71
+ ),
72
+ $defaults
73
+ );
74
+ }
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Gets default sub field.
80
+ *
81
+ * @return array
82
+ */
83
+ protected function get_default_sub_field() {
84
+ return array(
85
+ 'type' => 'text',
86
+ 'label' => '',
87
+ 'classes' => '',
88
+ 'wrapper_classes' => '',
89
+ 'options' => array(
90
+ 'default_value',
91
+ 'placeholder',
92
+ 'desc',
93
+ ),
94
+ 'optional' => false,
95
+ 'atts' => array(),
96
+ );
97
+ }
98
+
99
+ /**
100
+ * Registers extra options for saving.
101
+ *
102
+ * @return array
103
+ */
104
+ protected function extra_field_opts() {
105
+ $extra_options = parent::extra_field_opts();
106
+
107
+ // Register for sub field options.
108
+ foreach ( $this->sub_fields as $key => $sub_field ) {
109
+ if ( empty( $sub_field['options'] ) || ! is_array( $sub_field['options'] ) ) {
110
+ continue;
111
+ }
112
+
113
+ foreach ( $sub_field['options'] as $option ) {
114
+ if ( 'default_value' === $option ) { // We parse default value from field column.
115
+ continue;
116
+ }
117
+
118
+ if ( is_string( $option ) ) {
119
+ $extra_options[ $key . '_' . $option ] = '';
120
+ } elseif ( ! empty( $option['name'] ) ) {
121
+ $extra_options[ $key . '_' . $option['name'] ] = '';
122
+ }
123
+ }
124
+ }
125
+
126
+ return $extra_options;
127
+ }
128
+
129
+ /**
130
+ * Include the settings for placeholder, default value, and sub labels for each
131
+ * of the individual field labels.
132
+ *
133
+ * @since 4.0
134
+ * @param array $args Includes 'field', 'display'.
135
+ */
136
+ public function show_after_default( $args ) {
137
+ $field = (array) $args['field'];
138
+ $default_value = $this->get_default_value();
139
+ $processed_sub_fields = $this->get_processed_sub_fields();
140
+
141
+ foreach ( $this->sub_fields as $name => $sub_field ) {
142
+ $sub_field['name'] = $name;
143
+ $wrapper_classes = 'frm_grid_container frm_sub_field_options frm_sub_field_options-' . $sub_field['name'];
144
+ if ( ! isset( $processed_sub_fields[ $name ] ) ) {
145
+ // Options for this subfield should be hidden.
146
+ $wrapper_classes .= ' frm_hidden';
147
+ }
148
+
149
+ include FrmAppHelper::plugin_path() . '/classes/views/frm-fields/back-end/combo-field/sub-field-options.php';
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Gets default value of field.
155
+ * This should return an array of default value of sub fields.
156
+ *
157
+ * @return array
158
+ */
159
+ protected function get_default_value() {
160
+ $default_value = $this->get_field_column( 'default_value' );
161
+
162
+ if ( is_array( $default_value ) ) {
163
+ return $default_value;
164
+ }
165
+
166
+ if ( is_object( $default_value ) ) {
167
+ return (array) $default_value;
168
+ }
169
+
170
+ if ( ! $default_value ) {
171
+ $default_value = array();
172
+
173
+ foreach ( $this->sub_fields as $name => $sub_field ) {
174
+ $default_value[ $name ] = '';
175
+ }
176
+
177
+ return $default_value;
178
+ }
179
+
180
+ return json_decode( $default_value, true ); // We store default value as JSON string in db.
181
+ }
182
+
183
+ /**
184
+ * Gets labels for built-in options of fields or sub fields.
185
+ *
186
+ * @return array
187
+ */
188
+ protected function get_built_in_option_labels() {
189
+ return array(
190
+ 'default_value' => __( 'Default Value', 'formidable' ),
191
+ 'placeholder' => __( 'Placeholder Text', 'formidable' ),
192
+ 'desc' => __( 'Description', 'formidable' ),
193
+ );
194
+ }
195
+
196
+ /**
197
+ * Which built-in settings this field supports?
198
+ *
199
+ * @return array
200
+ */
201
+ protected function field_settings_for_type() {
202
+ $settings = array(
203
+ 'description' => false,
204
+ 'default' => false,
205
+ 'clear_on_focus' => false, // Don't use the regular placeholder option.
206
+ 'logic' => true,
207
+ );
208
+
209
+ return $settings;
210
+ }
211
+
212
+ /**
213
+ * Shows field on the form builder.
214
+ *
215
+ * @param string $name Field name.
216
+ */
217
+ public function show_on_form_builder( $name = '' ) {
218
+ $field = FrmFieldsHelper::setup_edit_vars( $this->field );
219
+
220
+ $field['default_value'] = $this->get_default_value();
221
+ $field['value'] = $field['default_value'];
222
+
223
+ $field_name = $this->html_name( $name );
224
+
225
+ $this->load_field_output( compact( 'field', 'field_name' ) );
226
+ }
227
+
228
+ /**
229
+ * Gets processed sub fields.
230
+ * This should return the list of sub fields after sorting or show/hide based of some options.
231
+ *
232
+ * @return array
233
+ */
234
+ protected function get_processed_sub_fields() {
235
+ return $this->sub_fields;
236
+ }
237
+
238
+ /**
239
+ * Shows field in the frontend.
240
+ *
241
+ * @param array $args Arguments.
242
+ * @param array $shortcode_atts Shortcode attributes.
243
+ * @return string
244
+ */
245
+ public function front_field_input( $args, $shortcode_atts ) {
246
+ $field = (array) $this->field;
247
+
248
+ $field['default_value'] = $this->get_default_value();
249
+ if ( empty( $field['value'] ) ) {
250
+ $field['value'] = $field['default_value'];
251
+ }
252
+
253
+ $args['field'] = $field;
254
+ $args['shortcode_atts'] = $shortcode_atts;
255
+
256
+ ob_start();
257
+ $this->load_field_output( $args );
258
+ $input_html = ob_get_clean();
259
+
260
+ return $input_html;
261
+ }
262
+
263
+ /**
264
+ * Loads field output.
265
+ *
266
+ * @param array $args {
267
+ * Arguments.
268
+ *
269
+ * @type array $field Field array.
270
+ * @type string $html_id HTML ID.
271
+ * @type string $field_name Field name attribute.
272
+ * @type array $shortcode_atts Shortcode attributes.
273
+ * @type array $errors Field errors.
274
+ * @type bool $remove_names Remove name attribute or not.
275
+ * }
276
+ */
277
+ protected function load_field_output( $args ) {
278
+ if ( empty( $args['field'] ) ) {
279
+ return;
280
+ }
281
+
282
+ $this->process_args_for_field_output( $args );
283
+
284
+ $include_paths = array(
285
+ FrmAppHelper::plugin_path() . "/classes/views/frm-fields/front-end/{$args['field']['type']}-field/{$args['field']['type']}-field.php",
286
+ FrmAppHelper::plugin_path() . '/classes/views/frm-fields/front-end/combo-field/combo-field.php',
287
+ );
288
+
289
+ foreach ( $include_paths as $include_path ) {
290
+ if ( file_exists( $include_path ) ) {
291
+ include $include_path;
292
+ return;
293
+ }
294
+ }
295
+ }
296
+
297
+ /**
298
+ * Loads processed args for field output.
299
+ *
300
+ * @param array $args {
301
+ * Arguments.
302
+ *
303
+ * @type array $field Field array.
304
+ * @type string $html_id HTML ID.
305
+ * @type string $field_name Field name attribute.
306
+ * @type array $shortcode_atts Shortcode attributes.
307
+ * @type array $errors Field errors.
308
+ * @type bool $remove_names Remove name attribute or not.
309
+ * }
310
+ */
311
+ protected function process_args_for_field_output( &$args ) {
312
+ $args['field'] = (array) $args['field'];
313
+
314
+ if ( ! isset( $args['html_id'] ) ) {
315
+ $args['html_id'] = $this->html_id();
316
+ }
317
+
318
+ if ( ! isset( $args['field_name'] ) ) {
319
+ $args['field_name'] = $this->html_name( $args['field']['name'] );
320
+ }
321
+
322
+ $args['sub_fields'] = $this->get_processed_sub_fields();
323
+
324
+ if ( ! isset( $args['shortcode_atts'] ) ) {
325
+ $args['shortcode_atts'] = array();
326
+ }
327
+
328
+ if ( ! isset( $args['errors'] ) ) {
329
+ $args['errors'] = array();
330
+ }
331
+ }
332
+
333
+ /**
334
+ * Prints sub field input atts.
335
+ *
336
+ * @param array $args Arguments. Includes `field`, `sub_field`.
337
+ */
338
+ protected function print_input_atts( $args ) {
339
+ $field = $args['field'];
340
+ $sub_field = $args['sub_field'];
341
+ $atts = array();
342
+
343
+ // Placeholder.
344
+ if ( in_array( 'placeholder', $sub_field['options'], true ) ) {
345
+ $placeholders = FrmField::get_option( $field, 'placeholder' );
346
+ if ( ! empty( $placeholders[ $sub_field['name'] ] ) ) {
347
+ $atts[] = 'placeholder="' . esc_attr( $placeholders[ $sub_field['name'] ] ) . '"';
348
+ }
349
+ }
350
+
351
+ // Add optional class.
352
+ $classes = isset( $sub_field['classes'] ) ? $sub_field['classes'] : '';
353
+ if ( is_array( $classes ) ) {
354
+ $classes = implode( ' ', $classes );
355
+ }
356
+
357
+ if ( ! empty( $sub_field['optional'] ) ) {
358
+ $classes .= ' frm_optional';
359
+ }
360
+
361
+ if ( $classes ) {
362
+ $atts[] = 'class="' . esc_attr( $classes ) . '"';
363
+ }
364
+
365
+ // Print custom attributes.
366
+ if ( ! empty( $sub_field['atts'] ) && is_array( $sub_field['atts'] ) ) {
367
+ foreach ( $sub_field['atts'] as $att_name => $att_value ) {
368
+ $atts[] = esc_attr( trim( $att_name ) ) . '="' . esc_attr( trim( $att_value ) ) . '"';
369
+ }
370
+ }
371
+
372
+ echo implode( ' ', $atts ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
373
+ }
374
+
375
+ /**
376
+ * Validate field.
377
+ *
378
+ * @param array $args Arguments. Includes `errors`, `value`.
379
+ * @return array Errors array.
380
+ */
381
+ public function validate( $args ) {
382
+ $errors = isset( $args['errors'] ) ? $args['errors'] : array();
383
+
384
+ if ( ! $this->field->required ) {
385
+ return $errors;
386
+ }
387
+
388
+ if ( class_exists( 'FrmProEntryMeta' ) && FrmProEntryMeta::skip_required_validation( $this->field ) ) {
389
+ return $errors;
390
+ }
391
+
392
+ $blank_msg = FrmFieldsHelper::get_error_msg( $this->field, 'blank' );
393
+
394
+ $sub_fields = $this->get_processed_sub_fields();
395
+
396
+ // Validate not empty.
397
+ foreach ( $sub_fields as $name => $sub_field ) {
398
+ if ( empty( $sub_field['optional'] ) && empty( $args['value'][ $name ] ) ) {
399
+ $errors[ 'field' . $args['id'] . '-' . $name ] = '';
400
+ $errors[ 'field' . $args['id'] ] = $blank_msg;
401
+ }
402
+ }
403
+
404
+ return $errors;
405
+ }
406
+
407
+ /**
408
+ * Gets export headings.
409
+ *
410
+ * @return array
411
+ */
412
+ public function get_export_headings() {
413
+ $headings = array();
414
+ $field_id = isset( $this->field->id ) ? $this->field->id : $this->field['id'];
415
+ $field_name = isset( $this->field->name ) ? $this->field->name : $this->field['name'];
416
+ $field_key = isset( $this->field->field_key ) ? $this->field->field_key : $this->field['field_key'];
417
+ $sub_fields = $this->get_processed_sub_fields();
418
+ foreach ( $sub_fields as $name => $sub_field ) {
419
+ $headings[ $field_id . '_' . $name ] = $field_name . ' (' . $field_key . ') - ' . $sub_field['label'];
420
+ }
421
+
422
+ return $headings;
423
+ }
424
+
425
+ /**
426
+ *
427
+ * Get a list of all field settings that should be translated
428
+ * on a multilingual site.
429
+ *
430
+ * @since 3.06.01
431
+ *
432
+ * @return array
433
+ */
434
+ public function translatable_strings() {
435
+ $strings = parent::translatable_strings();
436
+
437
+ foreach ( $this->sub_fields as $name => $sub_field ) {
438
+ if ( in_array( 'desc', $sub_field['options'], true ) ) {
439
+ $strings[] = $name . '_desc';
440
+ }
441
+ }
442
+
443
+ return $strings;
444
+ }
445
+
446
+ /**
447
+ * Checks if should print hidden subfields and hide them. This is useful to use js to show or hide sub fields.
448
+ *
449
+ * @return bool
450
+ */
451
+ protected function should_print_hidden_sub_fields() {
452
+ return false;
453
+ }
454
+
455
+ /**
456
+ * Gets inputs container attributes.
457
+ *
458
+ * @return array
459
+ */
460
+ protected function get_inputs_container_attrs() {
461
+ return array(
462
+ 'class' => 'frm_combo_inputs_container',
463
+ 'id' => 'frm_combo_inputs_container_' . $this->field_id,
464
+ );
465
+ }
466
+ }
classes/models/fields/FrmFieldName.php ADDED
@@ -0,0 +1,227 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * Name field
4
+ *
5
+ * @package Formidable
6
+ * @since 4.11
7
+ */
8
+
9
+ if ( ! defined( 'ABSPATH' ) ) {
10
+ die( 'You are not allowed to call this page directly.' );
11
+ }
12
+
13
+ class FrmFieldName extends FrmFieldCombo {
14
+
15
+ /**
16
+ * Field name.
17
+ *
18
+ * @var string
19
+ * @since 3.0
20
+ */
21
+ protected $type = 'name';
22
+
23
+ public function __construct( $field = '', $type = '' ) {
24
+ parent::__construct( $field, $type );
25
+
26
+ $this->register_sub_fields(
27
+ array(
28
+ 'first' => __( 'First', 'formidable' ),
29
+ 'middle' => __( 'Middle', 'formidable' ),
30
+ 'last' => __( 'Last', 'formidable' ),
31
+ )
32
+ );
33
+ }
34
+
35
+ /**
36
+ * Gets processed sub fields.
37
+ * This should return the list of sub fields after sorting or show/hide based of some options.
38
+ *
39
+ * @return array
40
+ */
41
+ protected function get_processed_sub_fields() {
42
+ $name_layout = $this->get_name_layout();
43
+ $names = explode( '_', $name_layout );
44
+ $col_class = 'frm' . intval( 12 / count( $names ) );
45
+
46
+ $result = array();
47
+
48
+ foreach ( $names as $name ) {
49
+ if ( empty( $this->sub_fields[ $name ] ) ) {
50
+ continue;
51
+ }
52
+
53
+ if ( ! isset( $this->sub_fields[ $name ]['wrapper_classes'] ) ) {
54
+ $this->sub_fields[ $name ]['wrapper_classes'] = $col_class;
55
+ } elseif ( is_array( $this->sub_fields[ $name ]['wrapper_classes'] ) ) {
56
+ $this->sub_fields[ $name ]['wrapper_classes'] = implode( ' ', $this->sub_fields[ $name ]['wrapper_classes'] ) . ' ' . $col_class;
57
+ } else {
58
+ $this->sub_fields[ $name ]['wrapper_classes'] .= ' ' . $col_class;
59
+ }
60
+
61
+ $result[ $name ] = $this->sub_fields[ $name ];
62
+ }
63
+
64
+ return $result;
65
+ }
66
+
67
+ /**
68
+ * Gets name layout option value.
69
+ *
70
+ * @return string
71
+ */
72
+ protected function get_name_layout() {
73
+ $name_layout = FrmField::get_option( $this->field, 'name_layout' );
74
+ if ( ! $name_layout ) {
75
+ $name_layout = 'first_last';
76
+ }
77
+ return $name_layout;
78
+ }
79
+
80
+ /**
81
+ * Gets extra field options.
82
+ *
83
+ * @return string[]
84
+ */
85
+ protected function extra_field_opts() {
86
+ $extra_options = parent::extra_field_opts();
87
+
88
+ $extra_options['name_layout'] = 'first_last';
89