Simple contact form with field validation

You don’t have any validation mechanism.

Your logic should be somewhat along those lines

  • Submit form
  • Check submitted fields ($_POST) against expected values
  • If all looks good send
  • If something is not as expected, log error ( you can use WP_Error() ) and rebuild form showing error message (and maybe repopulating fields with previous “good” values).

All I see here is you sanitize the inputs, but you don’t actually validate if your inputs have the values you expect (i.e. valid email, phone, length of name, etc.).

You send your email regardless if your fields have the expected values. Your else will output an error ONLY if wp_mail() fails, not if your actual fields have validated values or not.

To add an honey pot, you simply need to add an hidden field in your form that you expect to be empty.

For instance, in your HTML

<input type="text" name="content" id="content" value="" class="hpot" />

Then when you validate your form input, you expect that field to be empty.

using WP_Error class you can add errors to the object for later using them to inform the user or whatnot.

$errors = new WP_Error();
if ( isset( $_POST[ 'content' ] ) && $_POST[ 'content' ] !== '' ) {
  $errors->add( 'cheater', 'Sorry, this field should not be filled. Are you trying to cheat?' );
}

So the PHP check above is a way that you could use to validate your form. You simply add some if statements with expected values of your form (of course this can be expanded to function validating your input). Then, if you use WP_Error class, by adding to the object if errors are found, you just have to do a final check before sending.

if ( empty( $errors->errors ) ){
  deliver_mail();
}
else {
  // Here you can use your $_POST variable to repopulate the form with accepted 
  // form value (you would have to update your html_form_code() to accept an argument) 
  // or just reload the contact page displaying an error message.
}

EDIT

Ok so here’s a more complete example

CSS

add this to your css so the field is actually not displayed in the browser

.hpot {
  display: none;
}

PHP

Here’s another way of writing your html function, it’s just easier to read

function html_form_code() { ?>

<form action="<?php esc_url( $_SERVER['REQUEST_URI'] ); ?>" method="post">
  <p>Your Name (required)<br />
    <input type="text" name="cf-name" pattern="[a-zA-Z0-9 ]+" value="<?php isset( $_POST["cf-name"] ) ? esc_attr( $_POST["cf-name"] ) : ''; ?>" size="40" />
  </p>
  <p>Your Email (required)<br />
    <input type="email" name="cf-email" value="<?php isset( $_POST["cf-email"] ) ? esc_attr( $_POST["cf-email"] ) : ''; ?>" size="40" />
  </p>
  <p>Your Message (required)<br />
    <textarea rows="10" cols="35" name="cf-message"><?php isset( $_POST["cf-message"] ) ? esc_attr( $_POST["cf-message"] ) : ''; ?></textarea>
  </p>
  <p><input type="submit" name="cf-submitted" value="Send"/></p>
</form>

<?php } 

Your deliver_mail function should not listen for $_POST and should not sanitize. On a side note, using the forms user email as a from header could cause some issues with some ISP, because the email is sent from your domain and now an email is sent from your domain but with non matching domain in the from address (could be seen as spam). Use an address from your domain here (like [email protected]) and set the user’s email in the body of the email (in the message). You could also set it as a reply-to field for convenience of use.

function deliver_mail( $args = array() ) {

  // This $default array is a way to initialize some default values that will be overwritten by our $args array.
  // We could add more keys as we see fit and it's a nice way to see what parameter we are using in our function.
  // It will only be overwritten with the values of our $args array if the keys are present in $args.
  // This uses WP wp_parse_args() function.
  $defaults = array(
    'name'    => '',
    'email'   => '',
    'message' => '',
    'to'      => get_option( 'admin_email' ), // get the administrator's email address
  );

  $args = wp_parse_args( $args, $defaults );

  $headers = "From: {$args['name']} <{$args['email']}>" . "\r\n";

  // Send email returns true on success, false otherwise
  if( wp_mail( $args['to'], $args['message'], $headers ) ) {
    return;
  }
  else {
    return false;
  }
}

Your validation function

function my_validate_form() {

  $errors = new WP_Error();

  if ( isset( $_POST[ 'content' ] ) && $_POST[ 'content' ] !== '' ) {
    $errors->add( 'cheater', 'Sorry, this field should not be filled. Are you trying to cheat?' );
  }

  if ( isset( $_POST[ 'cf-name' ] ) && $_POST[ 'cf-name' ] == '' ) {
    $errors->add('name_error', 'Please fill in a valid name.' );
  }

  if ( isset( $_POST[ 'cf-email' ] ) && $_POST[ 'cf-email' ] == '' ) {
    $errors->add('email_error', 'Please fill in a valid email.' );
  }

  if ( isset( $_POST[ 'cf-message' ] ) && $_POST[ 'cf-message' ] == '' ) {
    $errors->add('message_error', 'Please fill in a valid message.' );
  }

  return $errors;
}

Your sanitization function. Here is a general sanitization function trimming white spaces and escaping html, but this could be more complex depending on the input fields you have. But I think for your purpose it’s enough

function my_sanitize_field( $input ){

  return trim( stripslashes( sanitize_text_field ( $input ) ) );

}

Displaying your success/error message, you could use this to retrieve the WP_Error object

function my_form_message(){

  global $errors;
  if( is_wp_errors( $errors ) && empty( $errors->errors ) ){

    echo '<div class="cf-success">';
    echo '<p>Thank you for contacting us '. $_POST['cf-name'] .', a member of our team will be in touch with you shortly.</p>';
    echo '</div>';

    //Empty $_POST because we already sent email
    $_POST = '';

  }
  else {

  if( is_wp_errors( $errors ) && ! empty( $errors->errors ) ){

    $error_messages = $errors->get_error_messages(); 
    foreach( $error_messages as $k => $message ){
        echo '<div class="cf-error ' . $k . '">';
        echo '<p>' . $message . '</p>';
        echo '</div>';

    }

  }

}

Finally your shortcode function

add_shortcode( 'contact_form', 'cf_contact_form' );
function cf_contact_form() {

  ob_start();

  my_form_message();
  html_form_code();

  return ob_get_clean();
}

And hooking to init to listen for $_POST before we render our form and output the $errors if we find some or send if all is ok.

add_action( 'init', 'my_cf_form');
function my_cf_form(){

  if( isset( $_POST['cf-submitted'] ) ) {

    global $errors;
    $errors = my_validate_form(); 
    if( empty( $errors->errors ) ){

       $args = array(
         'name'    => my_sanitize_field( $_POST['cf-name'] ),
         'email'   => my_sanitize_field( $_POST['cf-email'] ),
         'message' => my_sanitize_field( $_POST['cf-message'] ),
       );
       deliver_mail( $args );
    }
    else {
      return $errors;
    } 
  }
}

Remember to always prefix your functions so you don’t conflict with other plugins function names.