21

I have a custom post type Event that contains a starting and ending date/times custom fields (as metaboxes in the post edit screen).

I would like to make sure that an Event cannot get published (or scheduled) without the dates being filled, as that will cause problems with the templates displaying the Event data (besides the fact that it is a necessary requirement!). However, I would like to be able to have Draft events that do not contain a valid date while they are in preparation.

I was thinking of hooking save_post to do the checking, but how can I prevent the status change from happening?

EDIT1: This is the hook I'm using now to save the post_meta.

// Save the Metabox Data
function ep_eventposts_save_meta( $post_id, $post ) {

if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE )
    return;

if ( !isset( $_POST['ep_eventposts_nonce'] ) )
    return;

if ( !wp_verify_nonce( $_POST['ep_eventposts_nonce'], plugin_basename( __FILE__ ) ) )
    return;

// Is the user allowed to edit the post or page?
if ( !current_user_can( 'edit_post', $post->ID ) )
    return;

// OK, we're authenticated: we need to find and save the data
// We'll put it into an array to make it easier to loop though

//debug
//print_r($_POST);

$metabox_ids = array( '_start', '_end' );

foreach ($metabox_ids as $key ) {
    $events_meta[$key . '_date'] = $_POST[$key . '_date'];
    $events_meta[$key . '_time'] = $_POST[$key . '_time'];
    $events_meta[$key . '_timestamp'] = $events_meta[$key . '_date'] . ' ' . $events_meta[$key . '_time'];
}

$events_meta['_location'] = $_POST['_location'];

if (array_key_exists('_end_timestamp', $_POST))
    $events_meta['_all_day'] = $_POST['_all_day'];

// Add values of $events_meta as custom fields

foreach ( $events_meta as $key => $value ) { // Cycle through the $events_meta array!
    if ( $post->post_type == 'revision' ) return; // Don't store custom data twice
    $value = implode( ',', (array)$value ); // If $value is an array, make it a CSV (unlikely)
    if ( get_post_meta( $post->ID, $key, FALSE ) ) { // If the custom field already has a value
        update_post_meta( $post->ID, $key, $value );
    } else { // If the custom field doesn't have a value
        add_post_meta( $post->ID, $key, $value );
    }
    if ( !$value ) 
                delete_post_meta( $post->ID, $key ); // Delete if blank
}

}

add_action( 'save_post', 'ep_eventposts_save_meta', 1, 2 );

EDIT2: and this is what I'm trying to use to check the post data after saving to the database.

add_action( 'save_post', 'ep_eventposts_check_meta', 99, 2 );
function ep_eventposts_check_meta( $post_id, $post ) {
//check that metadata is complete when a post is published
//print_r($_POST);

if ( $_POST['post_status'] == 'publish' ) {

    $custom = get_post_custom($post_id);

    //make sure both dates are filled
    if ( !array_key_exists('_start_timestamp', $custom ) || !array_key_exists('_end_timestamp', $custom )) {
        $post->post_status = 'draft';
        wp_update_post($post);

    }
    //make sure start < end
    elseif ( $custom['_start_timestamp'] > $custom['_end_timestamp'] ) {
        $post->post_status = 'draft';
        wp_update_post($post);
    }
    else {
        return;
    }
}
}

The main issue with this is a problem that was actually described in another question: using wp_update_post() within a save_post hook triggers an infinite loop.

EDIT3: I figured a way to do it, by hooking wp_insert_post_data instead of save_post. The only problem is that now the post_status is reverted, but now a misleading message saying "Post published" shows up (by adding &message=6 to the redirected URL), but the status is set to Draft.

add_filter( 'wp_insert_post_data', 'ep_eventposts_check_meta', 99, 2 );
function ep_eventposts_check_meta( $data, $postarr ) {
//check that metadata is complete when a post is published, otherwise revert to draft
if ( $data['post_type'] != 'event' ) {
    return $data;
}
if ( $postarr['post_status'] == 'publish' ) {
    $custom = get_post_custom($postarr['ID']);

    //make sure both dates are filled
    if ( !array_key_exists('_start_timestamp', $custom ) || !array_key_exists('_end_timestamp', $custom )) {
        $data['post_status'] = 'draft';
    }
    //make sure start < end
    elseif ( $custom['_start_timestamp'] > $custom['_end_timestamp'] ) {
        $data['post_status'] = 'draft';
    }
    //everything fine!
    else {
        return $data;
    }
}

return $data;
}

5 Answers 5

21

As m0r7if3r pointed out, there is no way of preventing a post from being published using the save_post hook, since the by the time that hook is fired, the post is already saved. The following, however, will allow you to revert the status without using wp_insert_post_data and without causing an infinite loop.

The following is not tested, but should work.

<?php
add_action('save_post', 'my_save_post');
function my_save_post($post_id) {
    if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE )
         return;

    if ( !isset( $_POST['ep_eventposts_nonce'] ) )
         return;

    if ( !wp_verify_nonce( $_POST['ep_eventposts_nonce'], plugin_basename( __FILE__ ) ) )
         return;

    // Is the user allowed to edit the post or page?
     if ( !current_user_can( 'edit_post', $post->ID ) )
         return;

   // Now perform checks to validate your data. 
   // Note custom fields (different from data in custom metaboxes!) 
   // will already have been saved.
    $prevent_publish= false;//Set to true if data was invalid.
    if ($prevent_publish) {
        // unhook this function to prevent indefinite loop
        remove_action('save_post', 'my_save_post');

        // update the post to change post status
        wp_update_post(array('ID' => $post_id, 'post_status' => 'draft'));

        // re-hook this function again
        add_action('save_post', 'my_save_post');
    }
}
?>

I've not checked, but looking at the code, the feedback message will display the incorrect message that the post was published. This is because WordPress redirects us to an url where the message variable is now incorrect.

To change it, we can use the redirect_post_location filter:

add_filter('redirect_post_location','my_redirect_location',10,2);
function my_redirect_location($location,$post_id){
    //If post was published...
    if (isset($_POST['publish'])){
        //obtain current post status
        $status = get_post_status( $post_id );

        //The post was 'published', but if it is still a draft, display draft message (10).
        if($status=='draft')
            $location = add_query_arg('message', 10, $location);
    }

    return $location;
}

To summarise the above redirect filter: If a post is set to be published, but is still a draft then we alter the message accordingly (which is message=10). Again, this is untested, but should work. The Codex of the add_query_arg suggests that when a variable is already it set, the function replaces it (but as I say, I haven't tested this).

3
  • Other than the missing ; on your add_query_arg line, this redirect_post_location filter trick is exactly what I needed. thanks! Apr 2, 2014 at 19:04
  • @MadtownLems fixed :) Apr 3, 2014 at 0:18
  • This was the only solution that worked for me (I didn't want any javascript hacks). You're the best!
    – Cogicero
    Jan 21, 2021 at 3:59
9

OK, this is finally how I ended up doing it: an Ajax call to a PHP function that does the checking, sort of inspired by this answer and using a clever tip from a question I asked on StackOverflow. Importantly, I make sure that only when we want to Publish the checking is done, so that a Draft can always be saved without the checking. This ended up being the easier solution to actually prevent the publication of the post. It might help someone else, so I wrote it up here.

First, add the necessary Javascript:

//AJAX to validate event before publishing
//adapted from https://wordpress.stackexchange.com/questions/15546/dont-publish-custom-post-type-post-if-a-meta-data-field-isnt-valid
add_action('admin_enqueue_scripts-post.php', 'ep_load_jquery_js');   
add_action('admin_enqueue_scripts-post-new.php', 'ep_load_jquery_js');   
function ep_load_jquery_js(){
global $post;
if ( $post->post_type == 'event' ) {
    wp_enqueue_script('jquery');
}
}

add_action('admin_head-post.php','ep_publish_admin_hook');
add_action('admin_head-post-new.php','ep_publish_admin_hook');
function ep_publish_admin_hook(){
global $post;
if ( is_admin() && $post->post_type == 'event' ){
    ?>
    <script language="javascript" type="text/javascript">
        jQuery(document).ready(function() {
            jQuery('#publish').click(function() {
                if(jQuery(this).data("valid")) {
                    return true;
                }
                var form_data = jQuery('#post').serializeArray();
                var data = {
                    action: 'ep_pre_submit_validation',
                    security: '<?php echo wp_create_nonce( 'pre_publish_validation' ); ?>',
                    form_data: jQuery.param(form_data),
                };
                jQuery.post(ajaxurl, data, function(response) {
                    if (response.indexOf('true') > -1 || response == true) {
                        jQuery("#post").data("valid", true).submit();
                    } else {
                        alert("Error: " + response);
                        jQuery("#post").data("valid", false);

                    }
                    //hide loading icon, return Publish button to normal
                    jQuery('#ajax-loading').hide();
                    jQuery('#publish').removeClass('button-primary-disabled');
                    jQuery('#save-post').removeClass('button-disabled');
                });
                return false;
            });
        });
    </script>
    <?php
}
}

Then, the function that handles the checking:

add_action('wp_ajax_ep_pre_submit_validation', 'ep_pre_submit_validation');
function ep_pre_submit_validation() {
//simple Security check
check_ajax_referer( 'pre_publish_validation', 'security' );

//convert the string of data received to an array
//from https://wordpress.stackexchange.com/a/26536/10406
parse_str( $_POST['form_data'], $vars );

//check that are actually trying to publish a post
if ( $vars['post_status'] == 'publish' || 
    (isset( $vars['original_publish'] ) && 
     in_array( $vars['original_publish'], array('Publish', 'Schedule', 'Update') ) ) ) {
    if ( empty( $vars['_start_date'] ) || empty( $vars['_end_date'] ) ) {
        _e('Both Start and End date need to be filled');
        die();
    }
    //make sure start < end
    elseif ( $vars['_start_date'] > $vars['_end_date'] ) {
        _e('Start date cannot be after End date');
        die();
    }
    //check time is also inputted in case of a non-all-day event
    elseif ( !isset($vars['_all_day'] ) ) {
        if ( empty($vars['_start_time'] ) || empty( $vars['_end_time'] ) ) {
            _e('Both Start time and End time need to be specified if the event is not an all-day event');
            die();              
        }
        elseif ( strtotime( $vars['_start_date']. ' ' .$vars['_start_time'] ) > strtotime( $vars['_end_date']. ' ' .$vars['_end_time'] ) ) {
            _e('Start date/time cannot be after End date/time');
            die();
        }
    }
}

//everything ok, allow submission
echo 'true';
die();
}

This function returns true if everything is fine, and submits the form to publish the post by the normal channel. Otherwise, the function returns an error message that is shown as an alert(), and the form is not submitted.

2
  • I have followed the same approach and getting the post being saved as "Draft" instead of "Publish" when the validation function returns true. Not sure how to fix that!!!<br/>Also not getting the data for a textarea field (ex. post_content, any other text area custom field) during the ajax call?
    – Mahmudur
    Aug 10, 2012 at 3:47
  • 2
    I have applied this solution a little bit differently: first of all I have used the below code in the javascript in case of success: delayed_autosave(); //get data from textarea/tinymce field jQuery('#publish').data("valid", true).trigger('click'); //publish post Many thanks.
    – Mahmudur
    Aug 13, 2012 at 2:25
3

I think that the best way to go about this is not to PREVENT the status change from happening so much as it is to REVERT it if it does. For example: You hook save_post, with a really high priority (so that the hook will fire very late, namely after you do your meta insert), then check the post_status of the post that's just been saved, and update it to pending (or draft or whatever) if it doesn't meet your criteria.

An alternate strategy would be to hook wp_insert_post_data to set the post_status directly. The disadvantage to this method, as far as I'm concerned, is that you will not have inserted the postmeta into the database yet, so you will have to process it, etc in place to do your checks, then process it again to insert it into the database...which could become a lot of overhead, either in performance or in code.

4
  • I am currently hooking save_post with priority 1 to save the meta fields from the metaboxes; what you are proposing then is to have a second hook to save_post with priority, say, 99? Would this ensure integrity? What if for some reason the first hook fires, the metadata is inserted and the post published, but the second hook doesn't, so you end up with invalid fields?
    – englebip
    Feb 12, 2012 at 12:00
  • I can't think of a situation where the first hook would fire but not the second...what kind of scenario are you thinking that could cause that? If you're that worried about it, you can insert the post meta, check the post meta, and then update the post_status all in one function running off of a single call to a hook if you'd prefer.
    – mor7ifer
    Feb 12, 2012 at 13:36
  • I posted my code as an edit to my question; I tried to use a second hook to save_post but that triggers an infinite loop.
    – englebip
    Feb 12, 2012 at 19:58
  • Your issue is that you should be checking the created post. So if( get_post_status( $post_id ) == 'publish' ) is what you want to be using, since you will be redefining the data in $wpdb->posts, not the data in $_POST[].
    – mor7ifer
    Feb 12, 2012 at 21:05
0

Best method may be JAVASCRIPT:

<script type="text/javascript">
var field_id =  "My_field_div__ID";    // <----------------- CHANGE THIS

var SubmitButton = document.getElementById("save-post") || false;
var PublishButton = document.getElementById("publish")  || false; 
if (SubmitButton)   {SubmitButton.addEventListener("click", SubmCLICKED, false);}
if (PublishButton)  {PublishButton.addEventListener("click", SubmCLICKED, false);}
function SubmCLICKED(e){   
  var passed= false;
  if(!document.getElementById(field_id)) { alert("I cant find that field ID !!"); }
  else {
      var Enabled_Disabled= document.getElementById(field_id).value;
      if (Enabled_Disabled == "" ) { alert("Field is Empty");   }  else{passed=true;}
  }
  if (!passed) { e.preventDefault();  return false;  }
}
</script>
-2

Sorry I cant give you a straight up answer but I do recall doing something similar very recently I just cant remember exactly how. I think I maybe did it around about way - something like I had it being a default value and if the person hadn't changed it I picked this up in an if statement so -> if(category==default category) {echo "You didn't pick a category!"; return them to the post creation page; } sorry this isn't a straight up answer but hope it helps a bit.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.