Prevent post from being published if custom fields not filled

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.

Read More

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;
}

Related posts

Leave a Reply

5 comments

  1. 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).

  2. 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.

  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. 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>
    
  5. 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.