Handling POST Requests the WordPress Way

We teamed up with SiteGround
To bring you up to 65% off web hosting, plus free access to the entire SitePoint Premium library (worth $99). Get SiteGround + SitePoint Premium Now

Get the Right File Permissions in Your WordPress Site with our tutorial.

An interactive website needs to be able to interact with user input, which is commonly in the form of form submissions. A WordPress site is no exception to this rule. There are various interactions happening on a website on a daily basis. For example, subscribing to a newsletter, sending a message to the site owner and filling in an order form. All of these usually happen from POST requests during a form submission.

WordPress Transients API

In this article, we’re going to take a simple example of handling a POST request from a user, which is submitted via a contact form. We are going to use an internal WordPress hook to properly get the data, process it accordingly and redirect the user back to a specific page.

This article assumes you have a basic knowledge of the WordPress Plugin API. If you’re not familiar, it’s highly recommended to review the Codex page first.

The Background

WordPress is based on an event driven architecture. This means internally, WordPress core is filled up with various actions and filters to modify the program execution or to alter the content during runtime. Examples of actions that are running during program execution are init, wp, template_redirect and wp_head. Many plugins utilise these actions and filters to modify how WordPress works.

It is no different with what we are going to achieve. All we need to know is the proper hooks needed for the POST request and to modify our code accordingly. This is possible by pointing all our form submissions to a specific file in the wp-admin directory called admin-post.php. If you have experienced integrating your application with WordPress internal AJAX API, then you will notice that the basic structure of admin-post.php is not much different to the admin-ajax.php counterpart.

Anatomy of admin-post.php

At the very basic level, admin-post.php only contains 71 lines of code. It starts by defining WP_ADMIN constant and then loading WordPress by requiring wp-load.php. After that it sends an appropriate header and triggers the admin_init action.

The current action is determined by this line of code:

$action = empty( $_REQUEST['action'] ) ? '' : $_REQUEST['action'];

Despite the name, admin-post.php can actually handle both of POST and GET requests. However, what we’re interested to go through in the context of this article will be only related to POST request. Next,

if ( ! wp_validate_auth_cookie() ) {
    if ( empty( $action ) ) {
        /**
         * Fires on a non-authenticated admin post request where no action was supplied.
         *
         * @since 2.6.0
         */
        do_action( 'admin_post_nopriv' );
    } else {
        /**
         * Fires on a non-authenticated admin post request for the given action.
         *
         * The dynamic portion of the hook name, `$action`, refers to the given
         * request action.
         *
         * @since 2.6.0
         */
        do_action( "admin_post_nopriv_{$action}" );
    }
} else {
    if ( empty( $action ) ) {
        /**
         * Fires on an authenticated admin post request where no action was supplied.
         *
         * @since 2.6.0
         */
        do_action( 'admin_post' );
    } else {
        /**
         * Fires on an authenticated admin post request for the given action.
         *
         * The dynamic portion of the hook name, `$action`, refers to the given
         * request action.
         *
         * @since 2.6.0
         */
        do_action( "admin_post_{$action}" );
    }
}

We can see that two different action hooks will be triggered based on the user logged in status which is admin_post for logged in user and admin_post_nopriv for the non logged in user. If you need a more refined control to only processing the data related to your request, a more specific action will also be triggered, namely admin_post_nopriv_{$action} and admin_post_{$action}, again based on the logged in state of the user.

Let’s say our POST request has an action with a value of foobar. These two actions will be triggered if the current user is not logged in:

  • admin_post_nopriv
  • admin_post_nopriv_foobar

If you are currently logged in, two different actions will be triggered:

  • admin_post
  • admin_post_foobar

For GET request, when you are accessing the URL like this:

http://www.example.com/wp-admin/admin-post.php?action=foobar&data=test

All the above four hooks are still available to you to use.

With this knowledge, we can easily hook into the appropriate actions to handle the contact form submission without actually messing with our theme template.

Proof of Concept

Let’s take a contact form as an example. The simplest way to do this (not recommended) is perhaps to create a custom page template based on the basic page.php, hardcode a form and do the processing in that file itself. This is roughly how it looks:

<?php
/*
 * Template Name: Contact Page
 */

get_header();

if ( ! empty( $_POST ) ) {
    // Sanitize the POST field
    // Generate email content
    // Send to appropriate email
}

?>

<div id="content">
    <form action="" method="post">
        <label for="fullname">Full Name</label>
        <input type="text" name="fullname" id="fullname" required>
        <label for="email">Email Address</label>
        <input type="email" name="email" id="email" required>
        <label for="message">Your Message</label>
        <textarea name="message" id="message"></textarea>
        <input type="submit" value="Send My Message">
    </form>
</div>

<?php
get_footer();

While this implementation will work, there is no separation or concern whatsoever which will lead to trouble in the long run. Maintaining the form is a nightmare as the processing is only happening in one place, and if we are to duplicate the form somewhere else, we need to redo the processing again.

To fully utilize the event driven nature of WordPress, and also to provide a separation of concern, we can make use of the admin-post.php provided. Converting the existing form to make it compatible with admin-post.php is fairly straightforward too.

We first change this line from:

<form action="" method="post">

to:

<form action="<?php echo esc_url( admin_url('admin-post.php') ); ?>" method="post">

To generate a proper URL pointing to the admin-post.php, we use the built-in function admin_url. This will make sure that our URL is always correct in reference to the current site that is running.

We also need to add the action hidden input so that we can trigger the more specific hook related to our contact form submission. Let’s use an example of contact_form for the action value.

We add this line somewhere in between the <form> tag.

<input type="hidden" name="action" value="contact_form">

This is how the page template looks like after the modification:

<?php
/*
 * Template Name: Contact Page
 */

get_header();
?>

<div id="content">
    <form action="<?php echo esc_url( admin_url('admin-post.php') ); ?>" method="post">
        <label for="fullname">Full Name</label>
        <input type="text" name="fullname" id="fullname" required>
        <label for="email">Email Address</label>
        <input type="email" name="email" id="email" required>
        <label for="message">Your Message</label>
        <textarea name="message" id="message"></textarea>
        <input type="hidden" name="action" value="contact_form">
        <input type="submit" value="Send My Message">
    </form>
</div>

<?php
get_footer();

Notice that we are removing the POST processing function on top of the template since we are going to hook into the action later on.

Actual Handling of the POST Request

For this part, we have two options, and going with either one is fine. We can either hook into the admin_post_* action via the functions.php of our theme, or we can create a simple plugin to handle the contact form. Let’s just code this into the functions.php for simplicity sake.

Remember that we have our custom action contact_form in the form, so by rights, we will have these four hooks available to us:

  • admin_post_nopriv
  • admin_post_nopriv_contact_form
  • admin_post
  • admin_post_contact_form

Open up functions.php of your current theme and add these lines in:

function prefix_send_email_to_admin() {
    /**
     * At this point, $_GET/$_POST variable are available
     *
     * We can do our normal processing here
     */ 

    // Sanitize the POST field
    // Generate email content
    // Send to appropriate email
}
add_action( 'admin_post_nopriv_contact_form', 'prefix_send_email_to_admin` );
add_action( 'admin_post_contact_form', 'prefix_send_email_to_admin` );

Since we want to handle the form submission the same way no matter if the user is currently logged in or not, we point to the same callback function for both admin_post_nopriv_contact_form and admin_post_contact_form. The actual callback function, prefix_send_email_to_admin will be your main point to do the data processing.

Conclusion

admin-post.php is a useful, hidden gem contained in the WordPress core. By utilizing it, we can keep the separation of concern between the actual template, and unrelated code that is not needed for display separated by hooking into the appropriate hooks for processing.

Get the Right File Permissions in Your WordPress Site with our tutorial.

We teamed up with SiteGround
To bring you up to 65% off web hosting, plus free access to the entire SitePoint Premium library (worth $99). Get SiteGround + SitePoint Premium Now