In this tutorial, you'll learn how to create a plugin that allows users to submit images and upload them to the WordPress media library. You'll also learn how to correctly delete images from the WordPress media library as well as do some basic validation on uploaded images.
Previously...
This tutorial is by request from some users who found my quotes plugin tutorial interesting but were especially keen to find out how the same technique could be used to upload images from the frontend. So here's a reiteration of that tutorial that does just that. For detailed info about plugin setup, shortcodes and nonces, see the previous tutorial.
The plugin will:
- display an image upload form using a shortcode
- accept only images of a certain type and maximum size
- add a custom post type for user images
- add images to the WordPress media library with a proper attachment caption
- display unpublished images
- allow users to delete their unpublished images
We will use the built-in WordPress post thumbnail (aka Featured Image) meta field to hold the image for each post. This also makes it easier to display and work with our image as we can use the post_thumbnail functions.
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
Create a plugin file named submit_user_images.php
in the wp-content/plugins/submit-user-images
directory.
Refer to the plugin source for plugin header info.
Step 2 Plugin Initialization Function
We're going to create a custom post type named user_images to hold our user images and a custom taxonomy named user_image_category. This will enable cleaner administration of the images 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', 'sui_plugin_init'); function sui_plugin_init(){ $image_type_labels = array( 'name' => _x('User images', 'post type general name'), 'singular_name' => _x('User Image', 'post type singular name'), 'add_new' => _x('Add New User Image', 'image'), 'add_new_item' => __('Add New User Image'), 'edit_item' => __('Edit User Image'), 'new_item' => __('Add New User Image'), 'all_items' => __('View User Images'), 'view_item' => __('View User Image'), 'search_items' => __('Search User Images'), 'not_found' => __('No User Images found'), 'not_found_in_trash' => __('No User Images found in Trash'), 'parent_item_colon' => '', 'menu_name' => 'User Images' ); $image_type_args = array( 'labels' => $image_type_labels, 'public' => true, 'query_var' => true, 'rewrite' => true, 'capability_type' => 'post', 'has_archive' => true, 'hierarchical' => false, 'map_meta_cap' => true, 'menu_position' => null, 'supports' => array('title', 'editor', 'author', 'thumbnail') ); register_post_type('user_images', $image_type_args); $image_category_labels = array( 'name' => _x( 'User Image Categories', 'taxonomy general name' ), 'singular_name' => _x( 'User Image', 'taxonomy singular name' ), 'search_items' => __( 'Search User Image Categories' ), 'all_items' => __( 'All User Image Categories' ), 'parent_item' => __( 'Parent User Image Category' ), 'parent_item_colon' => __( 'Parent User Image Category:' ), 'edit_item' => __( 'Edit User Image Category' ), 'update_item' => __( 'Update User Image Category' ), 'add_new_item' => __( 'Add New User Image Category' ), 'new_item_name' => __( 'New User Image Name' ), 'menu_name' => __( 'User Image Categories' ), ); $image_category_args = array( 'hierarchical' => true, 'labels' => $image_category_labels, 'show_ui' => true, 'query_var' => true, 'rewrite' => array( 'slug' => 'user_image_category' ), ); register_taxonomy('sui_image_category', array('user_images'), $image_category_args); $default_image_cats = array('humor', 'landscapes', 'sport', 'people'); foreach($default_image_cats as $cat){ if(!term_exists($cat, 'sui_image_category')) wp_insert_term($cat, 'sui_image_category'); } }
What this code does:
- uses the WordPress init action hook to call a plugin initialization function
- registers a custom post type named user_images
- registers a custom taxonomy named user_image_category and assigns it to the user_images post type
- adds some default categories to the user_image_category taxonomy if they don't already exist
We will now have a User Images menu in our admin dashboard and a way to administer user images and their categories.
Step 3 Set Up Some Defaults
We'll need to do some basic validation so let's define two constants for later use:
define('MAX_UPLOAD_SIZE', 200000); define('TYPE_WHITELIST', serialize(array( 'image/jpeg', 'image/png', 'image/gif' )));
Step 4 Define A Shortcode
We'll define a shortcode that will allow us to display (and process) the user images submission form in a post or page:
add_shortcode('sui_form', 'sui_form_shortcode');
Security
Because our plugin accepts data from the user, we implement the following security mechanisms:
- only logged-in users have access to the image submission form
- we use nonces to verify that the forms were generated by our plugin
- images are submitted using wp_insert_post which sanitizes the data before saving it to the database
- users can only view their own images, and nonces prevent them from deleting any other user's image posts
Step 5 The Main Function
This is the function called by our shortcode. It displays and processes the image submission form and the image listing/deletion form. We'll take it in bite-sized pieces and in Step 6 we'll look at the helper functions.
function sui_form_shortcode(){ if(!is_user_logged_in()){ return '<p>You need to be logged in to submit an image.</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['sui_upload_image_form_submitted'] ) && wp_verify_nonce($_POST['sui_upload_image_form_submitted'], 'sui_upload_image_form') ){ $result = sui_parse_file_errors($_FILES['sui_image_file'], $_POST['sui_image_caption']); if($result['error']){ echo '<p>ERROR: ' . $result['error'] . '</p>'; }else{ $user_image_data = array( 'post_title' => $result['caption'], 'post_status' => 'pending', 'post_author' => $current_user->ID, 'post_type' => 'user_images' ); if($post_id = wp_insert_post($user_image_data)){ sui_process_image('sui_image_file', $post_id, $result['caption']); wp_set_object_terms($post_id, (int)$_POST['sui_image_category'], 'sui_image_category'); } } }
- if the image form has been submitted, there'll be a sui_upload_image_form_submitted field which was generated by our wp_nonce_field function. We can then verify the nonce and proceed to process the submitted image
- do some validation by passing the file input (where the uploaded image data is stored) and caption input data to a validation function, sui_parse_file_errors, and display any returned errors
- construct an array setting the post status to pending (the admin will now have to approve it for publication), setting the post type to user_images (our custom post type), and setting the author of the image post to the currently logged-in user
- if the image post was successfully inserted, save the image in the WordPress media library (sui_process_image) and finally set the category for the image post and display a success message
if (isset( $_POST['sui_form_delete_submitted'] ) && wp_verify_nonce($_POST['sui_form_delete_submitted'], 'sui_form_delete')){ if(isset($_POST['sui_image_delete_id'])){ if($user_images_deleted = sui_delete_user_images($_POST['sui_image_delete_id'])){ echo '<p>' . $user_images_deleted . ' images(s) deleted!</p>'; } } }
- if the image delete form has been submitted, there'll be a sui_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 images checked for deletion
- we check that we actually have some images checked for deletion by testing $_POST['sui_image_delete_id']. If so, we hand them off to the sui_delete_user_images function (see Step 6)
- if images were deleted, we display a success message
echo sui_get_upload_image_form($sui_image_caption = $_POST['sui_image_caption'], $sui_image_category = $_POST['sui_image_category']); if($user_images_table = sui_get_user_images_table($current_user->ID)){ echo $user_images_table; }
- we output the image upload form
- finally, we output the images listing/deletion form by passing the user ID to the sui_get_user_images_table function (see Step 6)
Step 6 Helper Functions
Here we'll look at the functions that generate the forms, add the images to the media library and the function that deletes the selected images.
function sui_get_upload_image_form($sui_image_caption = '', $sui_image_category = 0){ $out = ''; $out .= '<form id="sui_upload_image_form" method="post" action="" enctype="multipart/form-data">'; $out .= wp_nonce_field('sui_upload_image_form', 'sui_upload_image_form_submitted'); $out .= '<label for="sui_image_caption">Image Caption - Letters, Numbers and Spaces</label><br/>'; $out .= '<input type="text" id="sui_image_caption" name="sui_image_caption" value="' . $sui_image_caption . '"/><br/>'; $out .= '<label for="sui_image_category">Image Category</label><br/>'; $out .= sui_get_image_categories_dropdown('sui_image_category', $sui_image_category) . '<br/>'; $out .= '<label for="sui_image_file">Select Your Image - ' . MAX_UPLOAD_SIZE . ' bytes maximum</label><br/>'; $out .= '<input type="file" size="60" name="sui_image_file" id="sui_image_file"><br/>'; $out .= '<input type="submit" id="sui_submit" name="sui_submit" value="Upload Image">'; $out .= '</form>'; return $out; }
- the function accepts 2 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 image categories by calling sui_get_image_categories_dropdown (see next function)
function sui_get_image_categories_dropdown($taxonomy, $selected){ return wp_dropdown_categories(array('taxonomy' => $taxonomy, 'name' => 'sui_image_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 user image categories from the user_image_category taxonomy (our custom taxonomy)
function sui_get_user_images_table($user_id){ $args = array( 'author' => $user_id, 'post_type' => 'user_images', 'post_status' => 'pending' ); $user_images = new WP_Query($args); if(!$user_images->post_count) return 0; $out = ''; $out .= '<p>Your unpublished images - Click to see full size</p>'; $out .= '<form method="post" action="">'; $out .= wp_nonce_field('sui_form_delete', 'sui_form_delete_submitted'); $out .= '<table id="user_images">'; $out .= '<thead><th>Image</th><th>Caption</th><th>Category</th><th>Delete</th></thead>'; foreach($user_images->posts as $user_image){ $user_image_cats = get_the_terms($user_image->ID, 'sui_image_category'); foreach($user_image_cats as $cat){ $user_image_cat = $cat->name; } $post_thumbnail_id = get_post_thumbnail_id($user_image->ID); $out .= wp_nonce_field('sui_image_delete_' . $user_image->ID, 'sui_image_delete_id_' . $user_image->ID, false); $out .= '<tr>'; $out .= '<td>' . wp_get_attachment_link($post_thumbnail_id, 'thumbnail') . '</td>'; $out .= '<td>' . $user_image->post_title . '</td>'; $out .= '<td>' . $user_image_cat . '</td>'; $out .= '<td><input type="checkbox" name="sui_image_delete_id[]" value="' . $user_image->ID . '" /></td>'; $out .= '</tr>'; } $out .= '</table>'; $out .= '<input type="submit" name="sui_delete" value="Delete Selected Images" />'; $out .= '</form>'; return $out; }
- accept the user ID because we need to get a listing of user images for the current user only
- create $args to specify our user, the post type of user_images and user images that are pending (not yet published by the admin)
- execute a custom query using WP_Query
- return false if our query returns no user images
- start a form and generate a nonce for the form
- loop through the images posts making sure we also grab the category of the image post
- generate a nonce for the image delete checkbox, assigning a unique name for the nonce by concatenating the user image post ID
- output a table row containing the image post info as well as a delete checkbox
Why add a nonce for each image post?
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 sui_delete_user_images($images_to_delete){ $images_deleted = 0; foreach($images_to_delete as $user_image){ if (isset($_POST['sui_image_delete_id_' . $user_image]) && wp_verify_nonce($_POST['sui_image_delete_id_' . $user_image], 'sui_image_delete_' . $user_image)){ if($post_thumbnail_id = get_post_thumbnail_id($user_image)){ wp_delete_attachment($post_thumbnail_id); } wp_trash_post($user_image); $images_deleted ++; } } return $images_deleted; }
- the function accepts an array of image post IDs to delete
- each image post ID is checked to see if a nonce was generated for it
- if the nonce verifies, we delete the image attachment that exists in the media library by passing the id of the image post thumbnail to the WordPress function wp_delete_attachment
- we also trash the image post using the WordPress function wp_trash_post
But doesn't the thumbnail attachment get deleted when the post is trashed?
No and that's because WordPress stores attachments as regular posts in the posts database table. Have a look yourself: all attachments are stored in the posts table with a post_type of attachment. Simply deleting a post of type user_images doesn't delete its thumbnail attachment. It remains in the media library for future use unless we specifically delete it with wp_delete_attachment. For our purposes, I thought it was best to remove the attachment when the user's post was deleted.
Step 7 The Image Handling Functions
Let's remind ourselves of what the output of an html file input looks like when it posts an image to your script:
Array ( [name] => ref_blind.jpg [type] => image/jpeg [tmp_name] => /tmp/php79xI4e [error] => 0 [size] => 106290 )
We pass that array to the sui_process_image function along with the id of the saved user image post and the santized image caption.
function sui_process_image($file, $post_id, $caption){ require_once(ABSPATH . "wp-admin" . '/includes/image.php'); require_once(ABSPATH . "wp-admin" . '/includes/file.php'); require_once(ABSPATH . "wp-admin" . '/includes/media.php'); $attachment_id = media_handle_upload($file, $post_id); update_post_meta($post_id, '_thumbnail_id', $attachment_id); $attachment_data = array( 'ID' => $attachment_id, 'post_excerpt' => $caption ); wp_update_post($attachment_data); return $attachment_id; }
- we need to include the WordPress admin scripts that handle image uploads behind the scenes
- we call the media_handle_upload function (which is part of media.php), passing it the uploaded file array and the post id
- now we have an attachment id which we can use with update_post_meta to assign the attachment to the post as its thumbnail. Note: "_thumbnail_id" refers to the internal thumbnail (Featured Image) meta field. Internal Wordpress fields begin with an underscore.
- we next use the attachment id to update the caption of the attachment using the wp_update_post function
Because attachments are just regular posts, if we update the post_excerpt field for the attachment, we are actually updating the attachment's caption field as seen in the media library edit screen.
The validation function
We also validate the file array and the user-provided image caption with the sui_parse_file_errors function.
function sui_parse_file_errors($file = '', $image_caption){ $result = array(); $result['error'] = 0; if($file['error']){ $result['error'] = "No file uploaded or there was an upload error!"; return $result; } $image_caption = trim(preg_replace('/[^a-zA-Z0-9\s]+/', ' ', $image_caption)); if($image_caption == ''){ $result['error'] = "Your caption may only contain letters, numbers and spaces!"; return $result; } $result['caption'] = $image_caption; $image_data = getimagesize($file['tmp_name']); if(!in_array($image_data['mime'], unserialize(TYPE_WHITELIST))){ $result['error'] = 'Your image must be a jpeg, png or gif!'; }elseif(($file['size'] > MAX_UPLOAD_SIZE)){ $result['error'] = 'Your image was ' . $file['size'] . ' bytes! It must not exceed ' . MAX_UPLOAD_SIZE . ' bytes.'; } return $result; }
- check the error element of the files array for an html upload error, if found return a result array with error
- run some regex on the image caption to remove everything but alphanumeric data and spaces, replacing with spaces for readability
- if we end up with an empty caption after sanitizing it, we return an error
- check the internal image type (don't trust the file extension) using the PHP getimagesize function against the TYPE_WHITELIST constant
- check the image size against the MAX_UPLOAD_SIZE constant
Step 8 Some Styling
Just drop this style info into the style.css file in your theme folder:
#sui_upload_image_form #sui_image_caption{ width:500px; } #user_images{ font-size:12px; } #user_images th{ text-align:left; } #user_images td{ vertical-align:middle; } #user_images td input{ margin:0px; }
Step 9 Try It Out
Activate the plugin, pop the shortcode onto a page, log into your site, and test it out. When you upload an image you'll see a new post appear under the User Images admin menu. It will be pending publication. You'll also see a new image listed in your media library, attached to your new post and with the caption as provided.
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 images for all users.
Final Thoughts
You may wish to place more stringent validation on your image uploads. Remember, you're accepting data from users who may accidentally or maliciously upload inappropriate files. Checking the file type and size is a good start.
Also, we created the attachment caption by updating the attachment's post_excerpt field. You can also set an attachment description by using the attachment's post_content field.
Comments