How to restrict an author scheduling their post date to a maximum X days ahead from the current date

It’s possible to add a custom dropdown box to the submit meta-box:

restriction

where the user can only select X days into the future.

If the date is too far, we set the post date to the current date.

Here’s how it looks like in action when I add a new post, change the date and publish it:

select

and we see here the scheduling.

Demo Plugin

Here’s one idea, that you can hopefully test further and adjust to your needs.

The configuration is of this form:

class Config
{
    const CPT       = 'post';
    const INTERVAL  = 8;
}

where the CPT is the custom post type to target and INTERVAL is the maximum allowed days into the future.

We use three classes:

  • Main for the plugin setup.
  • Dropdown that takes care of the custom dropdown box.
  • Restrictions to handle the restrictions when the post data is saved.

Here’s the plugin:

<?php
/**
 * Plugin Name: Publish Date Restrictions
 * Plugin URI:  http://wordpress.stackexchange.com/a/205252/26350
 * Author:      Birgir Erlendsson (birgire)
 * Version:     0.0.2
 */

namespace wpse\birgire;

add_action( 'admin_init', function()
{
    if( class_exists( 'wpse\birgire\Main' ) )
    {
        $main = new Main;
        $main->init( 
            new Dropdown, 
            new Restrictions 
        );
    }
} );

class Config
{
    const CPT       = 'post';
    const INTERVAL  = 8;
}

interface MainInterface 
{
    public function init( DropdownInterface $dropdown, RestrictionsInterface $restrictions );
    public function wp_insert_post_data( Array $data, Array $arr );
    public function do_meta_boxes();
    public function modified_meta_box( \WP_Post $post, Array $args );
}

interface DropdownInterface
{
    public function merge( \WP_Post $post, $html="" );
}

interface RestrictionsInterface
{
    public function post_data( Array $data, Array $arr );

}

class Dropdown implements DropdownInterface
{       
    private function get_options( \WP_Post $post )
    {
        // current post's Y-m-d time
        $post_ymd = ( new \DateTime( $post->post_date ) )->format( 'Y-m-d' );

        // Current local timestamp
        $now = current_time( 'timestamp' );

        // Construct custom +8days "Y-m-d" dropdown options
        $options="";
        for( $i = 0; $i < (int) Config::INTERVAL; $i++ )
        {
            // Next YMD according to the local time
            $ymd = ( new \DateTime )
                ->setTimestamp( $now )
                ->modify( sprintf( '+%dday', $i ) )
                ->format( 'Y-m-d' );

            // Label
            switch ( $i ) 
            {
                case 0:
                    $label = __( 'Today' );
                    break;
                case 1:
                    $label = __( 'Tomorrow' );
                    break;
                default:
                    $label = $ymd;
            }

            // Options
            $options .= sprintf( 
                '<option value="%s"%s>%s</option>', 
                $ymd, 
                selected( $ymd, $post_ymd, 0 ),
                $label 
            );

        } // end of for loop

        return $options;
    }

    private function get_style()
    {
        // Hide the current "Y, m, d" inputs 
        // Note: We can't remove it with preg_replace, because of Javascript checks
        return '
            <style>
                .timestamp-wrap label:nth-child(5),
                .timestamp-wrap label:nth-child(2),
                .timestamp-wrap label:nth-child(3),
                .timestamp-wrap label:nth-child(4) {
                    display:none !important;
                }
            </style>
        ';
    }

    private function get_html( \WP_Post $post )
    {
        // Construct custom +8days "Y-m-d" dropdown 
        return sprintf(
            '<label><span class="screen-reader-text">YMD</span>
                <select id="wpse_ymd" name="wpse_ymd">
                    <option value="">Select</option> %s     
                </select>
            </label>
            %s',
            $this->get_options( $post ),
            $this->get_style()
        );  
    }   

    public function merge( \WP_Post $post, $html="" )
    {
        // Create a +8days "Y-m-d" dropdown 
        $dropdown = $this->get_html( $post );

        // Inject into the metabox HTML
        $from = '<label><span class="screen-reader-text">Month</span>';
        $to  = $dropdown . $from;
        return str_replace( $from, $to, $html );
    }
}


class Restrictions implements RestrictionsInterface
{
    public function post_data( Array $data, Array $arr )
    {
        // Target the corresponding post type when the restricted 'wpse_ymd' is posted
        if(     
            ! empty( $data['post_type'] ) 
            && Config::CPT !== $data['post_type'] 
        )
            return $data;   

        // input date is valid
        $valid = false;

        // Current local datetime object
        $local = ( new \DateTime )->setTimestamp( current_time( 'timestamp' ) );

        // Process the selected custom dropdown date     
        if( ! empty( $arr['wpse_ymd'] ) ) 
        {
            // Create datetime objects
            $input_date_obj = ( new \DateTime )->createFromFormat( 'Y-m-d', $arr['wpse_ymd']   );   
            $post_date_obj  = ( new \DateTime )->createFromFormat( 'Y-m-d H:i:s', $data['post_date'] );

            // If valid date and not too far into the future!
            if(    $this->is_valid_date( $input_date_obj )
                && (int) Config::INTERVAL  >= $this->signed_diff_in_days( $local, $input_date_obj )
            ) {
                // Create mysql date strings
                $new_post_date_obj = $post_date_obj->setDate( 
                    $input_date_obj->format( 'Y' ), 
                    $input_date_obj->format( 'm' ), 
                    $input_date_obj->format( 'd' )
                );

                // Override current post_date and post_date_gmt 
                $data['post_date']      = $new_post_date_obj->format( 'Y-m-d H:i:s' );
                $data['post_date_gmt']  = $new_post_date_obj->setTimezone( new \DateTimeZone( 'GMT' ) )->format( 'Y-m-d H:i:s' );

                // Set status to 'future' if > now
                if( $new_post_date_obj->format( 'timestamp' ) > $now )
                    $data['post_status'] = 'future';

                $valid = true;
            }
        }

        // Set the post date to the current time, 
        // if user selected a date too far into the future
        if( ! $valid )
        {
            $data['post_date']      = $local->format( 'Y-m-d H:i:s' );
            $data['post_date_gmt']  = $local->setTimezone( new \DateTimeZone( 'GMT' ) )->format( 'Y-m-d H:i:s' );           
        }

        return $data;
    }

    private function is_valid_date( \DateTime $obj )
    {
        return 
               is_a( $obj, '\DateTime' ) 
            && 0 === $obj->getLastErrors()['error_count']
            && 0 === $obj->getLastErrors()['warning_count'];
    }

    private function signed_diff_in_days( \DateTime $dt1, \DateTime $dt2 )
    {
        $dt = $dt1->diff( $dt2 );

        // Calculate the number of days (both positive or negativve) 
        // See more here http://stackoverflow.com/a/22967760/2078474
        return $dt->days * ( $dt->invert ? -1 : 1 );        
    }

}

class Main implements MainInterface
{
    private $dropdown;
    private $restrictions;

    public function init( DropdownInterface $dropdown, RestrictionsInterface $restrictions  )
    {
        $this->restrictions  = $restrictions;
        $this->dropdown      = $dropdown;

        add_action( 'do_meta_boxes',        [ $this, 'do_meta_boxes' ] );       
        add_filter( 'wp_insert_post_data',  [ $this, 'wp_insert_post_data'], 10, 2 );
    }

    public function wp_insert_post_data( Array $data, Array $arr )
    {
        return $this->restrictions->post_data( $data, $arr );
    }

    public function do_meta_boxes()
    {
        // Remove submitdiv meta-box
        remove_meta_box( 
            'submitdiv', 
            sanitize_key( Config::CPT ), 
            'side' 
        );

        // Add it again, modified
        add_meta_box( 
            'submitdiv', 
            __( 'Publish' ), 
            [ $this, 'modified_meta_box' ], 
            sanitize_key( Config::CPT ), 
            'side', 
            'high' 
        );
    }

    public function modified_meta_box( \WP_Post $post, Array $args )
    {
        // Inject our custom +8days "Y-m-d" dropdown
        echo $this->dropdown->merge( 
            $post,
            $this->get_meta_box( $post, $args )  // Grab the current submit box
        );      
    }

    private function get_meta_box( \WP_Post $post, Array $args )
    {
        ob_start();
        post_submit_meta_box( $post, $args = [] );
        return ob_get_clean();      
    }


} // end class