In this tutorial, you'll learn how to create a plugin that allows users to submit form data. You'll also learn about security by using nonces.
What You'll Learn
- How to display and process a form using a shortcode
- Use nonces to secure user submissions
It's simpler than you think
Shortcodes are often used to display simple data, but because they are actually a way to branch out of a page or post and execute code, they can be used for quite complex tasks, such as displaying and processing forms.
We'll construct a plugin that will allow logged in users to:
- submit quotes for moderation and publication
- view their unpublished quotes
- delete their unpublished quotes
Here's what we're aiming for:
All code is available in the plugin source at the top of this tutorial.
Step 1 Set-Up the Plugin
The WordPress plugin folder is located in your WordPress installation folder at wp-content/plugins
. Create a folder inside the plugins folder. Let's call it submit-user-quotes
. Now, create the plugin file itself. Let's call it submit_user_quotes.php
. The path to your plugin file should now be: wp-content/plugins/submit-user-quotes/submit_user_quotes.php
Every Wordpress plugin needs some header information so WordPress can identify it and make it available on your dashboard plugin page.
<?php /* Plugin Name: Submit User Quotes Plugin URI: http://wp.rosselliot.co.nz/user-quotes/ Description: Allows registered users to submit quotes. Version: 1.0 License: GPLv2 Author: Ross Elliot Author URI: http://wp.rosselliot.co.nz */
You can edit this info as per your own requirements.
You'll see the plugin listed like this:
Step 2 Plugin Initialization Function
We're going to create a custom post type named Quotes to hold our quotes and a custom taxonomy named quote_category. This will enable cleaner administration of the quotes than simply assigning them to normal posts and categories.
The Init Hook and Function
We'll use the following initialization code to create our custom post type and custom taxonomy:
add_action('init', 'suq_plugin_init'); function suq_plugin_init(){ $quote_type_labels = array( 'name' => _x('Quotes', 'post type general name'), 'singular_name' => _x('Quote', 'post type singular name'), 'add_new' => _x('Add New Quote', 'quote'), 'add_new_item' => __('Add New Quote'), 'edit_item' => __('Edit Quote'), 'new_item' => __('Add New Quote'), 'all_items' => __('View Quotes'), 'view_item' => __('View Quote'), 'search_items' => __('Search Quotes'), 'not_found' => __('No Quotes found'), 'not_found_in_trash' => __('No Quotes found in Trash'), 'parent_item_colon' => '', 'menu_name' => 'Quotes' ); $quote_type_args = array( 'labels' => $quote_type_labels, 'public' => true, 'query_var' => true, 'rewrite' => true, 'capability_type' => 'post', 'has_archive' => true, 'hierarchical' => false, 'menu_position' => null, 'supports' => array('title', 'editor', 'author') ); register_post_type('quotes', $quote_type_args); $quote_category_labels = array( 'name' => _x( 'Quote Categories', 'taxonomy general name' ), 'singular_name' => _x( 'Quote', 'taxonomy singular name' ), 'search_items' => __( 'Search Quote Categories' ), 'all_items' => __( 'All Quote Categories' ), 'parent_item' => __( 'Parent Quote Category' ), 'parent_item_colon' => __( 'Parent Quote Category:' ), 'edit_item' => __( 'Edit Quote Category' ), 'update_item' => __( 'Update Quote Category' ), 'add_new_item' => __( 'Add New Quote Category' ), 'new_item_name' => __( 'New Quote Name' ), 'menu_name' => __( 'Quote Categories' ), ); $quote_category_args = array( 'hierarchical' => true, 'labels' => $quote_category_labels, 'show_ui' => true, 'query_var' => true, 'rewrite' => array( 'slug' => 'quote_category' ), ); register_taxonomy('quote_category', array('quotes'), $quote_category_args); $default_quote_cats = array('humor', 'politics', 'sport', 'philosophy'); foreach($default_quote_cats as $cat){ if(!term_exists($cat, 'quote_category')) wp_insert_term($cat, 'quote_category'); } }
What this code does:
- uses the WordPress init action hook to call a plugin initialization function
- registers a custom post type named Quotes
- registers a custom taxonomy named quote_category and assigns it to the Quotes post type
- adds some default categories to the quote_category taxonomy if they don't already exist
We will now have a Quotes menu in our admin dashboard and a way to administer quotes and their categories.
Step 3 Define A Shortcode
Next, we'll define a shortcode that will allow us to display (and process) the user quotes submission form in a post or page:
add_shortcode('suq_form', 'suq_form_shortcode');
Here we use the WordPress add_shortcode function to define a shortcode named suq_form and a function named suq_form_shortcode that will be called whenever WordPress encounters the shortcode [suq_form] in a post or a page.
Before we look at the form display and processing functions, let's talk a little about...
Security
Because our plugin accepts data from the user, we implement the following security mechanisms:
- only logged-in users have access to the post submission form
- we use nonces to verify that the forms were generated by our plugin
- quotes are submitted using wp_insert_post which sanitizes the data before saving it to the database
- users can only view their own quotes, and nonces prevent them from deleting any other user's quotes
Nonces
A nonce is a number used once. We use them to verify that the data coming back to us is actually from the forms we've created.
Here we generate a nonce field using wp_nonce_field that will be included in our form as a hidden field:
wp_nonce_field('suq_form_create_quote', 'suq_form_create_quote_submitted');
Because it's now a hidden field in our form, it'll come back to us when the form is submitted. We can then check that the nonce is valid using wp_verify_nonce:
wp_verify_nonce($_POST['suq_form_create_quote_submitted'], 'suq_form_create_quote') )
That will return true if the nonce verifies.
Step 4 The Main Function
This is the function called by our shortcode. It displays and processes the quote submission form and the quote listing/deletion form. We'll take it in bite-sized pieces and in Step 5 we'll look at the helper functions.
function suq_form_shortcode(){ if(!is_user_logged_in()){ return '<p>You need to be logged in to post a quote.</p>'; } global $current_user;
- check to see if the user is logged in
- grab the WordPress $current_user variable which we'll need to get our user ID
if (isset( $_POST['suq_form_create_quote_submitted'] ) && wp_verify_nonce($_POST['suq_form_create_quote_submitted'], 'suq_form_create_quote') ){ $suq_quote_author = trim($_POST['suq_quote_author']); $suq_quote_text = trim($_POST['suq_quote_text']); if($suq_quote_author != '' && $suq_quote_text != ''){ $quote_data = array( 'post_title' => $suq_quote_author, 'post_content' => $suq_quote_text, 'post_status' => 'pending', 'post_author' => $current_user->ID, 'post_type' => 'quotes' ); if($quote_id = wp_insert_post($quote_data)){ wp_set_object_terms( $quote_id, (int)$_POST['suq_quote_category'], 'quote_category'); echo '<p>Quote created and awaiting moderation!</p>'; } }else{//author or text field is empty echo '<p>Quote NOT saved! Who said it? and Quote must not be empty.</p>'; } }
- if the quote creation form has been submitted, there'll be a suq_form_create_quote_submitted field which was generated by our wp_nonce_field function. We can then verify the nonce and proceed to process the submitted quote
- do some basic validation by making sure the quote author and quote text fields both have something in them, if not, display error message
- construct an array setting the post status to pending (the admin will now have to approve it for publication), setting the post type to quotes (our custom post type), and setting the author of the quote to the currently logged-in user
- if the quote was successfully inserted, set the category for the quote and display a success message
if (isset( $_POST['suq_form_delete_submitted'] ) && wp_verify_nonce($_POST['suq_form_delete_submitted'], 'suq_form_delete')){ if(isset($_POST['suq_delete_id'])){ if($quotes_deleted = suq_delete_quotes($_POST['suq_delete_id'])){ echo '<p>' . $quotes_deleted . ' quote(s) deleted!</p>'; } } }
- if the quote delete form has been submitted, there'll be a suq_form_delete_submitted field which was generated by our wp_nonce_field function. We can then verify the nonce and proceed to process the array of quotes checked for deletion
- we check that we actually have some quotes checked for deletion by testing $_POST['suq_delete_id']. If so, we hand them off to the suq_delete_quotes function (see Step 5)
- if quotes were deleted, we display a success message
echo suq_get_create_quote_form($suq_quote_author, $suq_quote_text, $suq_quote_category); if($quotes_table = suq_get_user_quotes($current_user->ID)){ echo $quotes_table; }
- we output the quote creation form
- finally, we output the quote listing/deletion form by passing the user ID to the suq_get_user_quotes function (see Step 5)
Step 5 Helper Functions
Here we'll look at the functions that generate the forms and the function that deletes the selected quotes.
function suq_get_create_quote_form($suq_quote_author = '', $suq_quote_text = '', $suq_quote_category = 0){ $out .= '<form id="create_quote_form" method="post" action="">'; $out .= wp_nonce_field('suq_form_create_quote', 'suq_form_create_quote_submitted'); $out .= '<label for="suq_quote_author">Who said it?</label><br/>'; $out .= '<input type="text" id="suq_quote_author" name="suq_quote_author" value="' . $suq_quote_author . '"/><br/>'; $out .= '<label for="suq_quote_category">Category</label><br/>'; $out .= suq_get_quote_categories_dropdown('quote_category', $suq_quote_category) . '<br/>'; $out .= '<label for="suq_quote_text">Quote</label><br/>'; $out .= '<textarea id="suq_quote_text" name="suq_quote_text" />' . $suq_quote_text . '</textarea><br/><br/>'; $out .= '<input type="submit" id="suq_submit" name="suq_submit" value="Submit Quote For Publication">'; $out .= '</form>'; return $out; }
- the function accepts 3 optional arguments for repopulating the form fields. This is a convenience for the user.
- a nonce field is output which we check when the form is submitted
- we output a dropdown for the quote categories by calling suq_get_quote_categories_dropdown (see next function)
function suq_get_quote_categories_dropdown($taxonomy, $selected){ return wp_dropdown_categories(array('taxonomy' => $taxonomy, 'name' => 'suq_quote_category', 'selected' => $selected, 'hide_empty' => 0, 'echo' => 0)); }
- the function accepts 2 arguments including the element ID of the currently selected category
- we use the WordPress wp_dropdown_categories function to create a dropdown that lists the quote categories from the quote_category taxonomy (our custom taxonomy)
function suq_get_user_quotes($user_id){ $args = array( 'author' => $user_id, 'post_type' => 'quotes', 'post_status' => 'pending' ); $posts = new WP_Query($args); if(!$posts->post_count) return 0; $out .= '<p>Your Unpublished Quotes</p>'; $out .= '<form method="post" action="">'; $out .= wp_nonce_field('suq_form_delete', 'suq_form_delete_submitted'); $out .= '<table id="quotes">'; $out .= '<thead><th>Said By</th><th>Quote</th><th>Category</th><th>Delete</th></thead>'; foreach($posts->posts as $post){ $quote_cats = get_the_terms($post->ID, 'quote_category'); foreach($quote_cats as $cat){ $quote_cat = $cat->name; } $out .= wp_nonce_field('suq_post_delete_' . $post->ID, 'suq_post_delete_id_' . $post->ID, false); $out .= '<tr>'; $out .= '<td>' . $post->post_title . '</td>'; $out .= '<td>' . $post->post_content . '</td>'; $out .= '<td>' . $quote_cat . '</td>'; $out .= '<td><input type="checkbox" name="suq_delete_id[]" value="' . $post->ID . '" /></td>'; $out .= '</tr>'; } $out .= '</table>'; $out .= '<input type="submit" name="suq_delete" value="Delete Selected Quotes!">'; $out .= '</form>'; return $out; }
- accept the user ID because we need to get a listing of quotes for the current user only
- create $args to specify our user, the post type of quotes and quotes that are pending (not yet published by the admin)
- execute a custom query using WP_Query
- return false if our query returns no quotes
- start a form and generate a nonce for the form
- loop through the quotes making sure we also grab the category of the quote
- generate a nonce for the quote delete checkbox, assigning a unique name for the nonce by concatenating the post ID
- output a table row containing the quote info as well as a delete checkbox
Why add a nonce for each quote?
Forms can be manipulated in the browser to post back unexpected data. In our case, each delete checkbox is assigned the value of a post. But what if a malicious user altered that value and caused our delete function to remove a post that was not actually listed?
One way to avoid this, is to use nonces for each row of post data, ensuring that the nonces are uniquely named with the post value to be deleted. We then verify the nonce upon form submission to make sure it's a genuine return value.
function suq_delete_quotes($quotes_to_delete){ $quotes_deleted = 0; foreach($quotes_to_delete as $quote){ if (isset($_POST['suq_post_delete_id_' . $quote]) && wp_verify_nonce($_POST['suq_post_delete_id_' . $quote], 'suq_post_delete_' . $quote)){ wp_trash_post($quote); $quotes_deleted ++; } } return $quotes_deleted; }
- the function accepts an array of quote IDs to delete
- each quote ID is checked to see if a nonce was generated for it
- if the nonce verifies, we trash the quote using the Wordpress function wp_trash_post
Step 6 Some Styling
Just drop this style info into the style.css file in your theme folder:
#suq_quote_author{ width:300px; } #suq_quote_text{ width:400px; height:100px; } #quotes{ font-size:12px; } #quotes th{ text-align:left; }
Step 7 Try It Out
Activate the plugin, pop the shortcode onto a page, log into your site, and test it out.
The full plugin code source and a demo site link is listed at the top of this tutorial.
The source folder also contains a Wordpress page template with a custom loop that displays published quotes for all users.
Final Thoughts
- the quotes plugin could be improved by offering an edit option. As it is, users can only delete their quotes
- you could also include an image upload option to brighten things up
- perhaps add some custom fields to the quotes post type for quote meta info
Comments