Video icon 64
Learning to code? Skill up faster with our practical video courses. Start your free trial today.
Advertisement

Attaching Files To Your Posts Using WordPress Custom Meta Boxes, Part 2

by

In the first post, we took a look at how to attach a file - specifically, a PDF - to WordPress posts and pages without having to use a plugin or third-party solution. At this point, you can only upload files - there's no way to actually deactivate the link or delete the link to the file once it has been uploaded. In this post, we'll take a look at how to provide some slightly better styling for the download link and how to extend the custom meta box functionality by allowing users to delete files after they've downloaded them.


Dress It Up

If you followed along with the code in the first post, then you should have a functional demo of how attaching a PDF to a WordPress post (or page) works; however, the overall presentation doesn't look very good:

Before going any further, let's clean this up a bit so that it looks a bit more integrated with the default theme. Remember that we're using Twentyeleven as our default theme so that we're all on the same page as we work through the tutorial.

First, let's move the download link into a more logical location. If you've been following along since the first post, you'll recall that we placed the download link in single.php which is located in the root of the theme directory. Open the file and locate the block of code that looks like this:

get_template_part( 'content', 'single' );
$doc = get_post_meta(get_the_ID(), 'wp_custom_attachment', true);
	<a href="	<?php echo $doc['url']; ?>">
	Download PDF Here
	</a>

Copy all but the first line of code and remove it from the file. This should leave only a call to get_template_part.

Next, locate custom-single.php. This is a template file located in the root of the theme directory. Find the call to the_content() and then paste the code you just copied directly below it. This should result in the following block of code:

the_content();
$doc = get_post_meta(get_the_ID(), 'wp_custom_attachment', true);
	<a href="	<?php echo $doc['url']; ?>">
	Download PDF Here
	</a>

Now, let's wrap the link in a container so that we can easily style it. I'm giving my container the ID of 'wp_custom_attachment.' Feel free to use whatever you like, just remember to refer to it correctly in your stylesheet.

Here's my markup:

the_content();
$doc = get_post_meta(get_the_ID(), 'wp_custom_attachment', true);
<div id="wp_custom_attachment">
	<a href="<?php echo $doc['url']; ?>">
		Download PDF Here
	</a> 
</div><!-- #wp_custom_attachment -->

And here's my CSS:

.wp_custom_attachment { 
	margin: 8px 0 8px 0;
	border: 1px solid #DDD;
	background: #EEE;
	padding: 8px;
	text-align: center;
	border-radius: 4px;
}

Permitting you've written everything correctly, the download link should now look like this:

Much better, right? It's now styled such that it looks to be more tightly integrated with the theme. Remember to make the corresponding changes to page.php, as well (considering that we're supporting file attachments on both posts and pages).


Download, When Available

Before moving on, we have one more small change to make to the code we just added. Right now, the download link is displayed unconditionally. This means that whether or not a post actually has a file, we're displaying the download link.

Ideally, we only want to display the download link whenever there is a file to download. As such, we'll need a conditional. Specifically, we'll need to get the associated post meta data, determine if there is an actual URL for the file attachment. If so, we'll display the link; otherwise, we won't.

In the block of code we just added to content-single.php, place an opening if statement just above the opening wp_custom_attachment tag and close the statement just below the closing container tag. Right now, the code should look something like this:

the_content();
$doc = get_post_meta(get_the_ID(), 'wp_custom_attachment', true);
if() { 
	<div id="wp_custom_attachment">
		<a href="<?php echo $doc['url']; ?>">
			Download PDF Here
		</a> 
	</div><!-- #wp_custom_attachment -->
} // end if

We need to check the presence of the document's URL. There are a number of ways to do this, but I typically check by taking a look at its URL attribute. The resulting code block is:

the_content();
$doc = get_post_meta(get_the_ID(), 'wp_custom_attachment', true);
if(strlen(trim($doc['url'])) > 0) { 
	<div id="wp_custom_attachment">
		<a href="<?php echo $doc['url']; ?>">
			Download PDF Here
		</a> 
	</div><!-- #wp_custom_attachment -->
} // end if

At this point, the download link should only display when there is a valid file attached to the give post or page. Try it out.


Deleting The File

At this point, we've tied up some loose ends from the previous post and we're ready to finish up the functionality necessary to delete attachments.

Laying The Foundation

Recall from the last post that once the user attempts to upload a file, we have a serialization function that fires that's responsible for actually writing the file to disk. For reference, this takes place in save_custom_meta_data.

In order to properly delete a file, we need to track the existing file's location and whether or not a user has actually requested to delete the file. We'll do that with a combination of an input box and an anchor.

First, locate the 'wp_custom_attachment' function that we created in the last post. This is where we added the file input element. Just below the input element add the following code (the full function will be provided below):

// Create the input box and set the file's URL as the text element's value
$html .= '<input type="text" id="wp_custom_attachment_url" name="wp_custom_attachment_url" value=" ' . $doc['url'] . '" size="30" />';

This will add an a text input element which tracks the value of the uploaded document's URL. We'll clean this up a bit later in the tutorial, but for now, let's introduce an an anchor for deleting the file. Just below the two lines of code we just added, write the following:

$html .= '<a href="javascript:;" id="wp_custom_attachment_delete">' . __('Delete File') . '</a>';

Take note: we've given the anchor a unique ID. This will be necessary when we begin hooking up the administration area to handle user events. If you don't give your anchor this ID, make note of whatever you do assign.

We're not quite done yet. Remember how we setup the single post and page views to conditionally display the download link? We need to do the same thing for the delete link. Specifically, we need to only show the delete link if a document exists. So, in similar fashion, wrap the anchor in a conditional statement that checks for the presence of the document's URL:

// Display the 'Delete' option if a URL to a file exists
if(strlen(trim($doc['url'])) > 0) {
	$html .= '<a href="javascript:;" id="wp_custom_attachment_delete">' . __('Delete File') . '</a>';
} // end if

It's nothing too heavy, right? To be complete, here's the full function as it stands:

function wp_custom_attachment() {

	wp_nonce_field(plugin_basename(__FILE__), 'wp_custom_attachment_nonce');
	
	$html = '<p class="description">';
		$html .= 'Upload your PDF here.';
	$html .= '</p>';
	$html .= '<input type="file" id="wp_custom_attachment" name="wp_custom_attachment" value="" size="25" />';
	
	// Grab the array of file information currently associated with the post
	$doc = get_post_meta(get_the_ID(), 'wp_custom_attachment', true);
	
	// Create the input box and set the file's URL as the text element's value
	$html .= '<input type="text" id="wp_custom_attachment_url" name="wp_custom_attachment_url" value=" ' . $doc['url'] . '" size="30" />';
	
	// Display the 'Delete' option if a URL to a file exists
	if(strlen(trim($doc['url'])) > 0) {
		$html .= '<a href="javascript:;" id="wp_custom_attachment_delete">' . __('Delete File') . '</a>';
	} // end if
	
	echo $html;

} // end wp_custom_attachment

We'll revisit this function a little but later in the tutorial but, for now, test it out. First, navigate to a post that has no attachment. You should see the file input box and an empty input box. After uploading a file, you should see the input box contain the URL of the file followed by a link for deleting the file.

But we're not done yet. After all, the delete link doesn't actually do anything.

Wiring It Up

Next, locate the 'js' directory in the theme root. Add a new file called custom_attachment.js. We'll write code for this momentarily, but the purpose of the file will be what allows us to actually delete the PDF that we've attached to a post.

After that, open up functions.php and add the following function at the end of the file:

function add_custom_attachment_script() {

	wp_register_script('custom-attachment-script', get_stylesheet_directory_uri() . '/js/custom_attachment.js');
	wp_enqueue_script('custom-attachment-script');

} // end add_custom_attachment_script
add_action('admin_enqueue_scripts', 'add_custom_attachment_script');

This function will read the JavaScript file that we just created and include it on any administrative page in the WordPress backend. Enqueuing and Registering scripts is beyond the scope of the tutorial, but I recommend reading up on it.

Next, let's revisit the JavaScript file. Generally speaking, the code should do the following things:

  • Determine if the delete link is present
  • If the link is present, attach a custom event handler that clears out the text input that contains the URL of the file
  • Hide the link once the file has been marked for deletion

The source code is below and it has been fully commented to help explain what each line is doing:

jQuery(function($) {

	// Check to see if the 'Delete File' link exists on the page...
	if($('a#wp_custom_attachment_delete').length === 1) {

		// Since the link exists, we need to handle the case when the user clicks on it...
		$('#wp_custom_attachment_delete').click(function(evt) {
		
			// We don't want the link to remove us from the current page
			// so we're going to stop it's normal behavior.
			evt.preventDefault();
			
			// Find the text input element that stores the path to the file
			// and clear it's value.
			$('#wp_custom_attachment_url').val('');
			
			// Hide this link so users can't click on it multiple times
			$(this).hide();
		
		});
	
	} // end if

});

At this point, the file will not be deleted but you should have a functional view. Locate a page that has a file attached to it. Your custom meta box should look something like this:

After clicking on the 'Delete Link' anchor, the custom meta box should look like this:

If not, double-check your debugging console to verify that you don't have any JavaScript errors.


Deleting The File

At this point, we've done all but actually delete the file. To do this, we'll need to update the save_custom_meta_data function that we wrote in the first post. Recall that the functional includes a conditional checks the contents of the $_FILES collection coming from the POST request. If the collection is populated, then we serialization the file.

Since we're attempting to delete the file, the $_FILES collection shouldn't contain any data so all of our code will need to be contained in an else clause. The full source code for the function will be provided below, but here's how the functional should work:

  • Check to see if there's a document associated with the post
  • Check to see if the text box used for tracking the file's URL is empty
  • If a file exists and the text box is empty, delete the file and update the associated meta data

This should be straightforward: We've given each post a text element that contains the URL to the file. If the file URL is empty, it means the user has clicked on the 'Delete File' link and is requesting to delete the file. Here's how we can achieve just that:

// Grab a reference to the file associated with this post
$doc = get_post_meta($id, 'wp_custom_attachment', true);

// Grab the value for the URL to the file stored in the text element
$delete_flag = get_post_meta($id, 'wp_custom_attachment_url', true);

// Determine if a file is associated with this post and if the delete flag has been set (by clearing out the input box)
if(strlen(trim($doc['url'])) > 0 && strlen(trim($delete_flag)) == 0) {

	// Attempt to remove the file. If deleting it fails, print a WordPress error.
	if(unlink($doc['file'])) {
		
		// Delete succeeded so reset the WordPress meta data
		update_post_meta($id, 'wp_custom_attachment', null);
		update_post_meta($id, 'wp_custom_attachment_url', '');
		
	} else {
		wp_die('There was an error trying to delete your file.');
	} // end if/el;se
	
} // end if

Once the file is deleted, note that we also have to update the post meta data by emptying out the attachment's value as well as the attachment's URL value. In the odd case that the file doesn't delete, we're displaying a simple error message. Advanced error handling is beyond the scope of this post.

As promised, here's the full serialization function:

function save_custom_meta_data($id) {

	/* --- security verification --- */
	if(!wp_verify_nonce($_POST['wp_custom_attachment_nonce'], plugin_basename(__FILE__))) {
	  return $id;
	} // end if
	  
	if(defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
	  return $id;
	} // end if
	  
	if(!current_user_can('edit_page', $id)) {
		return $id;
   	} // end if
	/* - end security verification - */
	
	// Make sure the file array isn't empty
	if(!empty($_FILES['wp_custom_attachment']['name'])) {
		
		// Setup the array of supported file types. In this case, it's just PDF.
		$supported_types = array('application/pdf');
		
		// Get the file type of the upload
		$arr_file_type = wp_check_filetype(basename($_FILES['wp_custom_attachment']['name']));
		$uploaded_type = $arr_file_type['type'];
		
		// Check if the type is supported. If not, throw an error.
		if(in_array($uploaded_type, $supported_types)) {

			// Use the WordPress API to upload the file
			$upload = wp_upload_bits($_FILES['wp_custom_attachment']['name'], null, file_get_contents($_FILES['wp_custom_attachment']['tmp_name']));
	
			if(isset($upload['error']) && $upload['error'] != 0) {
				wp_die('There was an error uploading your file. The error is: ' . $upload['error']);
			} else {
				add_post_meta($id, 'wp_custom_attachment', $upload);
				update_post_meta($id, 'wp_custom_attachment', $upload);		
			} // end if/else

		} else {
			wp_die("The file type that you've uploaded is not a PDF.");
		} // end if/else
		
	} else {

		// Grab a reference to the file associated with this post
		$doc = get_post_meta($id, 'wp_custom_attachment', true);
		
		// Grab the value for the URL to the file stored in the text element
		$delete_flag = get_post_meta($id, 'wp_custom_attachment_url', true);
		
		// Determine if a file is associated with this post and if the delete flag has been set (by clearing out the input box)
		if(strlen(trim($doc['url'])) > 0 && strlen(trim($delete_flag)) == 0) {
		
			// Attempt to remove the file. If deleting it fails, print a WordPress error.
			if(unlink($doc['file'])) {
				
				// Delete succeeded so reset the WordPress meta data
				update_post_meta($id, 'wp_custom_attachment', null);
				update_post_meta($id, 'wp_custom_attachment_url', '');
				
			} else {
				wp_die('There was an error trying to delete your file.');
			} // end if/el;se
			
		} // end if

	} // end if/else
	
} // end save_custom_meta_data
add_action('save_post', 'save_custom_meta_data');

By now, you've got a fully functioning custom meta box. Give it a try.


Cleaning It Up

We've got one last minor change to make just to make our UI complete. Remember the text input that we added earlier in the tutorial that's responsible for maintaining the file's URL? We can mark that as hidden - there's no reason the user needs to see it. The JavaScript source will still use it properly and its value will be read in the serialization function.

The final wp_custom_attachment function should look like this:

function wp_custom_attachment() {

	wp_nonce_field(plugin_basename(__FILE__), 'wp_custom_attachment_nonce');
	
	$html = '<p class="description">';
		$html .= 'Upload your PDF here.';
	$html .= '</p>';
	$html .= '<input type="file" id="wp_custom_attachment" name="wp_custom_attachment" value="" size="25" />';
	
	// Grab the array of file information currently associated with the post
	$doc = get_post_meta(get_the_ID(), 'wp_custom_attachment', true);
	
	// Create the input box and set the file's URL as the text element's value
	$html .= '<input type="hidden" id="wp_custom_attachment_url" name="wp_custom_attachment_url" value=" ' . $doc['url'] . '" size="30" />';
	
	// Display the 'Delete' option if a URL to a file exists
	if(strlen(trim($doc['url'])) > 0) {
		$html .= '<a href="javascript:;" id="wp_custom_attachment_delete">' . __('Delete File') . '</a>';
	} // end if
	
	echo $html;

} // end wp_custom_attachment

These two posts covered a lot of information. There are a number of canned solutions - be it plugins, themes, or other add-ons - available for integrating functionality like this, but part of being a good developer is knowing when to use a third-party solution and when to roll your own.

Additionally, if you're working with WordPress in a professional capacity, then it's important to understand the API. Hopefully this series has helped showcase much of what can be done by leveraging core functionality of WordPress.

Advertisement