· 8 years ago · Jul 29, 2018, 01:16 PM
1/**
2 * Limit Submissions Per Time Period (by IP, User, Role, Form URL, or Field Value)
3 *
4 * Limit the number of times a form can be submitted per a specific time period. You modify this limit to apply to
5 * the visitor's IP address, the user's ID, the user's role, a specific form URL, or the value of a specific field.
6 * These "limiters" can be combined to create more complex limitations.
7 *
8 */
9
10class GravityForms_Submission_Limit {
11
12 var $_args;
13 var $_notification_event;
14
15 private static $forms_with_individual_settings = array();
16
17 public function __construct( $args ) {
18
19 // make sure we're running the required minimum version of Gravity Forms
20 if( ! property_exists( 'GFCommon', 'version' ) || ! version_compare( GFCommon::$version, '1.8', '>=' ) )
21 return;
22
23 $this->_args = wp_parse_args( $args, array(
24 'form_id' => false,
25 'form_ids' => array(),
26 'limit' => 1,
27 'limit_by' => 'ip', // 'ip', 'user_id', 'role', 'embed_url', 'field_value'
28 'time_period' => 60 * 60 * 24, // integer in seconds or 'day', 'month', 'year' to limit to current day, month, or year respectively
29 'limit_message' => __( 'Sorry, you have reached the submission limit for this form.' ),
30 'apply_limit_per_form' => true,
31 'enable_notifications' => false
32 ) );
33
34 if( ! is_array( $this->_args['limit_by'] ) ) {
35 $this->_args['limit_by'] = array( $this->_args['limit_by'] );
36 }
37
38 if( empty( $this->_args['form_ids'] ) ) {
39 if( $this->_args['form_id'] === false ) {
40 $this->_args['form_ids'] = false;
41 } elseif( ! is_array( $this->_args['form_id'] ) ) {
42 $this->_args['form_ids'] = array( $this->_args['form_id'] );
43 } else {
44 $this->_args['form_ids'] = $this->_args['form_id'];
45 }
46 }
47
48 if( $this->_args['form_ids'] ) {
49 foreach( $this->_args['form_ids'] as $form_id ) {
50 self::$forms_with_individual_settings[] = $form_id;
51 }
52 }
53
54 add_action( 'init', array( $this, 'init' ) );
55
56 }
57
58 public function init() {
59
60 add_filter( 'gform_pre_render', array( $this, 'pre_render' ) );
61 add_filter( 'gform_validation', array( $this, 'validate' ) );
62
63 if( $this->_args['enable_notifications'] ) {
64
65 $this->enable_notifications();
66
67 add_action( 'gform_after_submission', array( $this, 'maybe_send_limit_reached_notifications' ), 10, 2 );
68
69 }
70
71 }
72
73 public function pre_render( $form ) {
74
75 if( ! $this->is_applicable_form( $form ) || ! $this->is_limit_reached( $form['id'] ) ) {
76 return $form;
77 }
78
79 $submission_info = rgar( GFFormDisplay::$submission, $form['id'] );
80
81 // if no submission, hide form
82 // if submission and not valid, hide form
83 // unless 'field_value' limiter is applied
84 if( ( ! $submission_info || ! rgar( $submission_info, 'is_valid' ) ) && ! $this->is_limited_by_field_value() ) {
85 add_filter( 'gform_get_form_filter_' . $form['id'], array( $this, 'get_limit_message' ) );
86 }
87
88 return $form;
89
90 }
91
92 public function get_limit_message() {
93 ob_start();
94 ?>
95 <div class="limit-message">
96 <?php echo do_shortcode( $this->_args['limit_message'] ); ?>
97 </div>
98 <?php
99 return ob_get_clean();
100 }
101
102 public function validate( $validation_result ) {
103
104 if( ! $this->is_applicable_form( $validation_result['form'] ) || ! $this->is_limit_reached( $validation_result['form']['id'] ) ) {
105 return $validation_result;
106 }
107
108 $validation_result['is_valid'] = false;
109
110 if( $this->is_limited_by_field_value() ) {
111 $field_ids = array_map( 'intval', $this->get_limit_field_ids() );
112 foreach( $validation_result['form']['fields'] as &$field ) {
113 if( in_array( $field['id'], $field_ids ) ) {
114 $field['failed_validation'] = true;
115 $field['validation_message'] = do_shortcode( $this->_args['limit_message'] );
116 }
117 }
118 }
119
120 return $validation_result;
121 }
122
123 public function is_limit_reached( $form_id ) {
124 return $this->get_entry_count( $form_id ) >= $this->get_limit();
125 }
126
127 public function get_entry_count( $form_id ) {
128 global $wpdb;
129
130 $where = array();
131 $join = array();
132
133 $where[] = 'l.status = "active"';
134
135 foreach( $this->_args['limit_by'] as $limiter ) {
136 switch( $limiter ) {
137 case 'role': // user ID is required when limiting by role
138 case 'user_id':
139 $where[] = $wpdb->prepare( 'l.created_by = %s', get_current_user_id() );
140 break;
141 case 'embed_url':
142 $where[] = $wpdb->prepare( 'l.source_url = %s', GFFormsModel::get_current_page_url());
143 break;
144 case 'field_value':
145
146 $values = $this->get_limit_field_values( $form_id, $this->get_limit_field_ids() );
147
148 // if there is no value submitted for any of our fields, limit is never reached
149 if( empty( $values ) ) {
150 return false;
151 }
152
153 foreach( $values as $field_id => $value ) {
154 $table_slug = sprintf( 'ld%s', str_replace( '.', '_', $field_id ) );
155 $join[] = "INNER JOIN {$wpdb->prefix}rg_lead_detail {$table_slug} ON {$table_slug}.lead_id = l.id";
156 //$where[] = $wpdb->prepare( "CAST( {$table_slug}.field_number as unsigned ) = %f AND {$table_slug}.value = %s", $field_id, $value );
157 $where[] = $wpdb->prepare( "\n( ( {$table_slug}.field_number BETWEEN %s AND %s ) AND {$table_slug}.value = %s )", doubleval( $field_id ) - 0.001, doubleval( $field_id ) + 0.001, $value );
158 }
159
160 break;
161 default:
162 $where[] = $wpdb->prepare( 'ip = %s', GFFormsModel::get_ip() );
163 }
164 }
165
166 if( $this->_args['apply_limit_per_form'] || ( ! $this->is_global( $form_id ) && count( $this->_args['form_ids'] ) <= 1 ) ) {
167 $where[] = $wpdb->prepare( 'l.form_id = %d', $form_id );
168 } else {
169 $where[] = $wpdb->prepare( 'l.form_id IN( %s )', implode( ', ', $this->_args['form_ids'] ) );
170 }
171
172 $time_period = $this->_args['time_period'];
173 $time_period_sql = false;
174
175 if( $time_period === false ) {
176 // no time period
177 } else if( intval( $time_period ) > 0 ) {
178 $time_period_sql = $wpdb->prepare( 'date_created BETWEEN DATE_SUB(utc_timestamp(), INTERVAL %d SECOND) AND utc_timestamp()', $this->_args['time_period'] );
179 } else {
180
181 $gmt_offset = get_option( 'gmt_offset' );
182 $date_func = $gmt_offset < 0 ? 'DATE_SUB' : 'DATE_ADD';
183 $hour_offset = abs( $gmt_offset );
184
185 $date_created_sql = sprintf( '%s( date_created, INTERVAL %d HOUR )', $date_func, $hour_offset );
186 $utc_timestamp_sql = sprintf( '%s( utc_timestamp(), INTERVAL %d HOUR )', $date_func, $hour_offset );
187
188 switch( $time_period ) {
189 case 'per_day':
190 case 'day':
191 $time_period_sql = "DATE( $date_created_sql ) = DATE( $utc_timestamp_sql )";
192 break;
193 case 'per_week':
194 case 'week':
195 $time_period_sql = "WEEK( $date_created_sql ) = WEEK( $utc_timestamp_sql )";
196 $time_period_sql .= "AND YEAR( $date_created_sql ) = YEAR( $utc_timestamp_sql )";
197 break;
198 case 'per_month':
199 case 'month':
200 $time_period_sql = "MONTH( $date_created_sql ) = MONTH( $utc_timestamp_sql )";
201 $time_period_sql .= "AND YEAR( $date_created_sql ) = YEAR( $utc_timestamp_sql )";
202 break;
203 case 'per_year':
204 case 'year':
205 $time_period_sql = "YEAR( $date_created_sql ) = YEAR( $utc_timestamp_sql )";
206 break;
207 }
208
209 }
210
211 if( $time_period_sql ) {
212 $where[] = $time_period_sql;
213 }
214
215 $where = implode( ' AND ', $where );
216 $join = implode( "\n", $join );
217
218 $sql = "SELECT count( l.id )
219 FROM {$wpdb->prefix}rg_lead l
220 $join
221 WHERE $where";
222
223 $entry_count = $wpdb->get_var( $sql );
224
225 return $entry_count;
226 }
227
228 public function is_limited_by_field_value() {
229 return in_array( 'field_value', $this->_args['limit_by'] );
230 }
231
232 public function get_limit_field_ids() {
233
234 $limit = $this->_args['limit'];
235
236 if( is_array( $limit ) ) {
237 $field_ids = array_keys( $this->_args['limit'] );
238 $field_ids = array( array_shift( $field_ids ) );
239 } else {
240 $field_ids = $this->_args['fields'];
241 }
242
243 return $field_ids;
244 }
245
246 public function get_limit_field_values( $form_id, $field_ids ) {
247
248 $form = GFAPI::get_form( $form_id );
249 $values = array();
250
251 foreach( $field_ids as $field_id ) {
252
253 $field = GFFormsModel::get_field( $form, $field_id );
254 if( ! $field ) {
255 continue;
256 }
257
258 $input_name = 'input_' . str_replace( '.', '_', $field_id );
259 $value = GFFormsModel::prepare_value( $form, $field, rgpost( $input_name ), $input_name, null );
260
261 if( ! rgblank( $value ) ) {
262 $values[ "$field_id" ] = $value;
263 }
264
265 }
266
267 return $values;
268 }
269
270 public function get_limit() {
271
272 $limit = $this->_args['limit'];
273
274 if( $this->is_limited_by_field_value() ) {
275 $limit = is_array( $limit ) ? array_shift( $limit ) : intval( $limit );
276 } else if( in_array( 'role', $this->_args['limit_by'] ) ) {
277 $limit = rgar( $limit, $this->get_user_role() );
278 }
279
280 return intval( $limit );
281 }
282
283 public function get_user_role() {
284
285 $user = wp_get_current_user();
286 $role = reset( $user->roles );
287
288 return $role;
289 }
290
291 public function enable_notifications() {
292
293 $event_slug = implode( array_filter( array( "submission_limit_limit_reached", $this->_args['form_id'] ) ) );
294 $event_name = GFForms::get_page() == 'notification_edit' ? __( 'Submission limit reached' ) : __( 'Event name is only populated on Notification Edit view; saves a DB call to get the form on every ' );
295
296 $this->_notification_event = new GW_Notification_Event( array(
297 'form_id' => $this->_args['form_id'],
298 'event_name' => $event_name,
299 'event_slug' => $event_slug
300 //'trigger' => array( $this, 'notification_event_listener' )
301 ) );
302
303 }
304
305 public function maybe_send_limit_reached_notifications( $entry, $form ) {
306
307 if( $this->is_applicable_form( $form ) && $this->is_limit_reached( $form['id'] ) ) {
308 $this->send_limit_reached_notifications( $form, $entry );
309 }
310
311 }
312
313 public function send_limit_reached_notifications( $form, $entry ) {
314
315 $this->_notification_event->send_notifications( $this->_notification_event->get_event_slug(), $form, $entry, true );
316
317 }
318
319 public function is_applicable_form( $form ) {
320
321 $form_id = isset( $form['id'] ) ? $form['id'] : $form;
322 $is_specific_form = ! $this->is_global( $form_id ) ? in_array( $form_id, $this->_args['form_ids'] ) : false;
323
324 return $this->is_global( $form_id ) || $is_specific_form;
325 }
326
327 public function is_global( $form) {
328 $form_id = isset( $form['id'] ) ? $form['id'] : $form;
329 return empty( $this->_args['form_ids'] ) && ! in_array( $form_id, self::$forms_with_individual_settings );
330 }
331
332}
333
334/**
335 * Email Domain Validator
336 *
337 * This snippets allows you to exclude a list of invalid domains or include a list of valid domains for your Gravity Form Email fields.
338 *
339 */
340class GravityForms_Email_Domain_Validator {
341
342 private $_args;
343
344 function __construct($args) {
345
346 $this->_args = wp_parse_args( $args, array(
347 'form_id' => false,
348 'field_id' => false,
349 'domains' => false,
350 'validation_message' => __( 'Sorry, <strong>%s</strong> email accounts are not eligible for this form.' ),
351 'mode' => 'ban' // also accepts "limit"
352 ) );
353
354 // convert field ID to an array for consistency, it can be passed as an array or a single ID
355 if($this->_args['field_id'] && !is_array($this->_args['field_id']))
356 $this->_args['field_id'] = array($this->_args['field_id']);
357
358 $form_filter = $this->_args['form_id'] ? "_{$this->_args['form_id']}" : '';
359
360 add_filter("gform_validation{$form_filter}", array($this, 'validate'));
361
362 }
363
364 function validate($validation_result) {
365
366 $form = $validation_result['form'];
367
368 foreach($form['fields'] as &$field) {
369
370 // if this is not an email field, skip
371 if(RGFormsModel::get_input_type($field) != 'email')
372 continue;
373
374 // if field ID was passed and current field is not in that array, skip
375 if($this->_args['field_id'] && !in_array($field['id'], $this->_args['field_id']))
376 continue;
377
378 $page_number = GFFormDisplay::get_source_page( $form['id'] );
379 if( $page_number > 0 && $field->pageNumber != $page_number ) {
380 continue;
381 }
382
383 if( GFFormsModel::is_field_hidden( $form, $field, array() ) ) {
384 continue;
385 }
386
387 $domain = $this->get_email_domain($field);
388
389 // if domain is valid OR if the email field is empty, skip
390 if($this->is_domain_valid($domain) || empty($domain))
391 continue;
392
393 $validation_result['is_valid'] = false;
394 $field['failed_validation'] = true;
395 $field['validation_message'] = sprintf($this->_args['validation_message'], $domain);
396
397 }
398
399 $validation_result['form'] = $form;
400 return $validation_result;
401 }
402
403 function get_email_domain( $field ) {
404 $email = explode( '@', rgpost( "input_{$field['id']}" ) );
405 return trim( rgar( $email, 1 ) );
406 }
407
408 function is_domain_valid( $domain ) {
409
410 $mode = $this->_args['mode'];
411 $domain = strtolower( $domain );
412
413 foreach( $this->_args['domains'] as $_domain ) {
414
415 $_domain = strtolower( $_domain );
416
417 $full_match = $domain == $_domain;
418 $suffix_match = strpos( $_domain, '.' ) === 0 && $this->str_ends_with( $domain, $_domain );
419 $has_match = $full_match || $suffix_match;
420
421 if( $mode == 'ban' && $has_match ) {
422 return false;
423 } else if( $mode == 'limit' && $has_match ) {
424 return true;
425 }
426
427 }
428
429 return $mode == 'limit' ? false : true;
430 }
431
432 function str_ends_with( $string, $text ) {
433
434 $length = strlen( $string );
435 $text_length = strlen( $text );
436
437 if( $text_length > $length ) {
438 return false;
439 }
440
441 return substr_compare( $string, $text, $length - $text_length, $text_length ) === 0;
442 }
443
444}
445
446/**
447* Pre-submission Confirmation
448*
449*/
450class GravityForms_Preview_Confirmation {
451
452 private static $lead;
453
454 public static function init() {
455 add_filter( 'gform_pre_render', array( __class__, 'replace_merge_tags' ) );
456 }
457
458 public static function replace_merge_tags( $form ) {
459
460 if( ! class_exists( 'GFFormDisplay' ) ) {
461 return $form;
462 }
463
464 $current_page = isset(GFFormDisplay::$submission[$form['id']]) ? GFFormDisplay::$submission[$form['id']]['page_number'] : 1;
465 $fields = array();
466
467 // get all HTML fields on the current page
468 foreach($form['fields'] as &$field) {
469
470 // skip all fields on the first page
471 if(rgar($field, 'pageNumber') <= 1)
472 continue;
473
474 $default_value = rgar($field, 'defaultValue');
475 preg_match_all('/{.+}/', $default_value, $matches, PREG_SET_ORDER);
476 if(!empty($matches)) {
477 // if default value needs to be replaced but is not on current page, wait until on the current page to replace it
478 if(rgar($field, 'pageNumber') != $current_page) {
479 $field['defaultValue'] = '';
480 } else {
481 $field['defaultValue'] = self::preview_replace_variables($default_value, $form);
482 }
483 }
484
485 // only run 'content' filter for fields on the current page
486 if(rgar($field, 'pageNumber') != $current_page)
487 continue;
488
489 $html_content = rgar($field, 'content');
490 preg_match_all('/{.+}/', $html_content, $matches, PREG_SET_ORDER);
491 if(!empty($matches)) {
492 $field['content'] = self::preview_replace_variables($html_content, $form);
493 }
494
495 }
496
497 return $form;
498 }
499
500 /**
501 * Adds special support for file upload, post image and multi input merge tags.
502 */
503 public static function preview_special_merge_tags($value, $input_id, $merge_tag, $field) {
504
505 // added to prevent overriding :noadmin filter (and other filters that remove fields)
506 if( ! $value )
507 return $value;
508
509 $input_type = RGFormsModel::get_input_type($field);
510
511 $is_upload_field = in_array( $input_type, array('post_image', 'fileupload') );
512 $is_multi_input = is_array( rgar($field, 'inputs') );
513 $is_input = intval( $input_id ) != $input_id;
514
515 if( !$is_upload_field && !$is_multi_input )
516 return $value;
517
518 // if is individual input of multi-input field, return just that input value
519 if( $is_input )
520 return $value;
521
522 $form = RGFormsModel::get_form_meta($field['formId']);
523 $lead = self::create_lead($form);
524 $currency = GFCommon::get_currency();
525
526 if(is_array(rgar($field, 'inputs'))) {
527 $value = RGFormsModel::get_lead_field_value($lead, $field);
528 return GFCommon::get_lead_field_display($field, $value, $currency);
529 }
530
531 switch($input_type) {
532 case 'fileupload':
533 $value = self::preview_image_value("input_{$field['id']}", $field, $form, $lead);
534 $value = self::preview_image_display($field, $form, $value);
535 break;
536 default:
537 $value = self::preview_image_value("input_{$field['id']}", $field, $form, $lead);
538 $value = GFCommon::get_lead_field_display($field, $value, $currency);
539 break;
540 }
541
542 return $value;
543 }
544
545 public static function preview_image_value($input_name, $field, $form, $lead) {
546
547 $field_id = $field['id'];
548 $file_info = RGFormsModel::get_temp_filename($form['id'], $input_name);
549 $source = RGFormsModel::get_upload_url($form['id']) . "/tmp/" . $file_info["temp_filename"];
550
551 if(!$file_info)
552 return '';
553
554 switch(RGFormsModel::get_input_type($field)){
555
556 case "post_image":
557 list(,$image_title, $image_caption, $image_description) = explode("|:|", $lead[$field['id']]);
558 $value = !empty($source) ? $source . "|:|" . $image_title . "|:|" . $image_caption . "|:|" . $image_description : "";
559 break;
560
561 case "fileupload" :
562 $value = $source;
563 break;
564
565 }
566
567 return $value;
568 }
569
570 public static function preview_image_display($field, $form, $value) {
571
572 // need to get the tmp $file_info to retrieve real uploaded filename, otherwise will display ugly tmp name
573 $input_name = "input_" . str_replace('.', '_', $field['id']);
574 $file_info = RGFormsModel::get_temp_filename($form['id'], $input_name);
575
576 $file_path = $value;
577 if(!empty($file_path)){
578 $file_path = esc_attr(str_replace(" ", "%20", $file_path));
579 $value = "<a href='$file_path' target='_blank' title='" . __("Click to view", "gravityforms") . "'>" . $file_info['uploaded_filename'] . "</a>";
580 }
581 return $value;
582
583 }
584
585 /**
586 * Retrieves $lead object from class if it has already been created; otherwise creates a new $lead object.
587 */
588 public static function create_lead( $form ) {
589
590 if( empty( self::$lead ) ) {
591 self::$lead = GFFormsModel::create_lead( $form );
592 self::clear_field_value_cache( $form );
593 }
594
595 return self::$lead;
596 }
597
598 public static function preview_replace_variables( $content, $form ) {
599
600 $lead = self::create_lead($form);
601
602 // add filter that will handle getting temporary URLs for file uploads and post image fields (removed below)
603 // beware, the RGFormsModel::create_lead() function also triggers the gform_merge_tag_filter at some point and will
604 // result in an infinite loop if not called first above
605 add_filter('gform_merge_tag_filter', array('GravityForms_Preview_Confirmation', 'preview_special_merge_tags'), 10, 4);
606
607 $content = GFCommon::replace_variables($content, $form, $lead, false, false, false);
608
609 // remove filter so this function is not applied after preview functionality is complete
610 remove_filter('gform_merge_tag_filter', array('GravityForms_Preview_Confirmation', 'preview_special_merge_tags'));
611
612 return $content;
613 }
614
615 public static function clear_field_value_cache( $form ) {
616
617 if( ! class_exists( 'GFCache' ) )
618 return;
619
620 foreach( $form['fields'] as &$field ) {
621 if( GFFormsModel::get_input_type( $field ) == 'total' )
622 GFCache::delete( 'GFFormsModel::get_lead_field_value__' . $field['id'] );
623 }
624
625 }
626
627}
628
629new GravityForms_Submission_Limit( array(
630 'limit' => 2,
631 'limit_message' => 'Sorry 2 Submissions per 24 hours permitted',
632 'limit_by' => 'ip',
633) );
634
635new GravityForms_Email_Domain_Validator( array(
636 'domains' => array( 'gmail.com', 'hotmail.com', '.co.uk' ),
637 'validation_message' => __( 'Oh no! <strong>%s</strong> email accounts are not allowed.' ),
638 'mode' => 'limit'
639) );
640
641GravityForms_Preview_Confirmation::init();