3

I am running a website with WordPress 4.2 and have changed the way draft posts or posts in review status are displayed to non logged in users (these posts are, too, visible to anyone who visits the website).

I have implemented this solution presented here: https://wordpress.org/support/topic/problem-filtering-query-to-allow-displaying-drafts-even-for-not-logged-users

Now, while anyone can now access draft posts / pending posts without having to log in, the posts are only accessible via the post_id, like so:

http://my-domain.com/?p=1234

They are not accessible via their slug / nice permalink, which used to be like that:

http://my-domain.com/2015/12/07/post-with-nice-slug/

Does anyone have an idea on how to "enable" permalinks for draft posts / pending posts?

Thank you!

UPDATE Feb. 20th

The answer given by @userabuser works like a charm!! Since all my pending / draft posts were previously published posts, I did not need to do the trick to publish first. I had thought I had come up with a working solution myself - but: here is the code I used before, which did not work (and I do not see why that is):

function guest_enable_hidden_single_post($query) {
    if(is_user_logged_in()) return $query;
    //user is not logged

    if(!is_single()) return $query;
    //this is a single post

    if(!$query->is_main_query())return $query;
    //this is the main query    

    $query->set('post_status',array('publish', 'pending'));
    //allowed post statuses for guest

    return $query;
}

function guest_reload_hidden_single_post($posts) {
    global $wp_query, $wpdb;

    if(is_user_logged_in()) return $posts;
    //user is not logged

    if(!is_single()) return $posts;
    //this is a single post

    if(!$wp_query->is_main_query()) return $posts;
    //this is the main query

    if($wp_query->post_count) return $posts;
    //no posts were found

    $posts = $wpdb->get_results($wp_query->request);

    return $posts;
}

//allow guests to view single posts even if they have not post_status="publish"
add_filter('pre_get_posts', 'guest_enable_hidden_single_post');

//reload hidden posts
add_filter('the_posts', 'guest_reload_hidden_single_post');

UPDATE Feb. 22nd

There was a problem with private pages not being displayed to admins / logged in users. I fixed that with the following code:

function preview_draft_posts($query) {
    if(is_user_logged_in()) return $query;
    // user is not logged

    if(is_admin()) return $query;
    // user is not admin

    if(is_attachment()) return $query;
    // is not an attachment

    if(get_query_var('suppress_filters')) return $query;
    // no suppressed filters

    if(!is_single()) return $query;
    // query for a single post

    if(!$query->is_main_query()) return $query;
    // this is the main query

    $query->set('post_status', array('publish', 'pending'));

    return $query;
}
add_filter('pre_get_posts', 'preview_draft_posts');

Now pending posts get displayed to anyone and private posts are still only visible to admins ;-)

1 Answer 1

4

Caveat: the code examples within this answer are very basic and may or may not need further conditional logic to adapt to your precise needs, this logic is meant as an example to get you on your way.

There's two considerations you need to be aware of:

Consideration 1:

If you add a new post and save it with a post_status of draft first, the post will not have the slug in the post_name field in the database *_posts table until you publish at least once.

Consideration 2:

If you add a new post, save it with a post_status of draft, or simply by pass the draft stage and go straight to publish then put the post back into a draft post_status, then the post will have the slug inserted into the post_name column within the *_posts table.

Why this is important:

  • In relation to consideration 1...

WordPress relies on at least one identifying marker, typically the id or the post_name which is unique in order to retrieve a post.

You will notice that when you create a draft post and attempt to preview it, WordPress takes you to a preview URL similar to:

http://example.com/?p=123&preview=true

It does this because the only way to identify and preview the post in question is to do it by the ID to which it is associated with in the databse.

  • In relation to consideration 2...

If you publish the post at least once, then put it back into draft, you will be able to access the post whilst in draft status using the slug (pretty permalink) because the post_name column contains the slug value.

Some example code:

The following code will allow you to return posts in a draft or pending status by using the slug so long as the post has been published at least once before (see consideration 2).

function preview_draft_posts($query) {

  if ( is_admin() || get_query_var('suppress_filters') )
    return $query;

   
  $query->set('post_status', array('publish', 'pending', 'draft'));


  return $query;

}

add_filter('pre_get_posts', 'preview_draft_posts');

The above will work for logged in and logged out users.

What happens if you are logged out and want to preview a draft?

Ok so if the draf has not been published at least once before, the only way to preview the post is by using the following format:

http://example.com/?p=123

WordPress depends on this format to find a post where it has no slug.

If you are logged out though you will not be able to see this post despite the pre_get_posts logic we have used in the example above.

Instead you need to hook onto the filter the_posts and add back the post to the results you want to view because WP_Query will exclude draft posts from the result set for logged out users because logged out users don't have the proper user-capabilities.

If on the other hand, you attempt (whilst logged out) to access a post similar to that in consideration 1 by it's default preview URL as mentioned above in the example http://example.com/?p=123&preview=true (with or without the preview=true) then you will be met with a 404 Page Not Found notice.

The following example code remedies that:

function the_posts_preview_draft_posts($posts, $wp_query) {

    //abort if $posts is not empty, this query ain't for us...
    if ( count($posts) ) {
        return $posts;
    }

    $p = get_query_var('p');

    //get our post instead and return it as the result...
    if ( !empty($p) ) {
        return array(get_post($p));
    }
}

add_filter('the_posts', 'the_posts_preview_draft_posts', 10, 2);

Great... but hang on, that still sucks, kind of, because you have to use a URL format such as http://example.com/?p=123 to access the post.

Back to the main problem outlined in consideration 1, the only solutions I can think of are the following:

Solution #1

  • save draft post and find the post by it's title by converting the slug into a title.

Example...

function the_posts_preview_draft_posts($posts, $wp_query) {
    
    //abort if $posts is not empty, this query ain't for us...
    if ( count($posts) ) {
        return $posts;
    }

    $id    = get_query_var('p');
    $name = get_query_var('name');

    //this is a bit of a bad idea...
    if ( empty($id) && !empty($name) ) {

        $name = preg_replace('/\W/', ' ', $name);

        global $wpdb;
        $id = $wpdb->get_var( 
            "
            SELECT ID 
            FROM $wpdb->posts 
            WHERE UPPER(post_title) LIKE UPPER('".$name."')
            LIMIT 1" 
        );

    }


    //get our post instead and return it as the result...
    if ( !empty($id) ) {
        return array(get_post($id));
    }

}

add_filter('the_posts', 'the_posts_preview_draft_posts', 10, 2);

The above... ↑ ...is a bad idea but just an example of the lengths you need to go to. Why this is a bad idea is simple, you need to ensure that you have no duplicate post titles in the database and you need to ensure that you do not modify the slug so that it is different to the title when you save it as a draft.

Fraught with issues as you can see.

Solution #2

Probably a better way to handle this would be to set the post_status of the post to publish so that the post_name column gets the slug value and then immediately put the post back into a draft status.

Doing that manually would suck so here's some code:

function allow_draft_public_preview( ) {

        global $post;

        if ( !empty($post->post_name) )
            return;

        $html =<<<DOC
        <div class="misc-pub-section">
            <label>
                <input type="checkbox" id="_allow_public_preview" name="_allow_public_preview" value="1" />
                Allow public preview
            </label>
        </div>
DOC;

echo $html;

}

add_action('post_submitbox_misc_actions', 'allow_draft_public_preview');

function update_post_status( $post_id, $post, $update ) {

    if ( !empty($_POST['_allow_public_preview']) ) {
        
        //un-hook to prevent infinite loop
        remove_action( 'save_post', 'update_post_status', 13, 2 );

        //set the post to publish so it gets the slug is saved to post_name
        wp_update_post( array( 'ID' => $post_id, 'post_status' => 'publish' ) );

        //immediately put it back to draft status
        wp_update_post( array( 'ID' => $post_id, 'post_status' => 'draft' ) );

        //re-hool
        add_action( 'save_post', 'update_post_status', 13, 2 );
    }

}

add_action('save_post', 'update_post_status', 13, 2);

The first callback attached to the post_submitbox_misc_actions action adds a checkbox to the Post edit screen "Allow public preview"...

enter image description here

...when you mark this checkbox it will be recognized by the second callback fired on the save_post action.

In the save_post action we check for the existence of our checkbox in the $_POST superglobal and if it's present, we set the post status to publish then immediately set it back to draft.

Now you can access draft posts, logged in or logged out, by their slugs/pretty permalinks.

All together now:

function preview_draft_posts($query) {

  if ( is_admin() || get_query_var('suppress_filters') )
    return $query;


  $query->set('post_status', array('publish', 'pending', 'draft'));


  return $query;

}

add_filter('pre_get_posts', 'preview_draft_posts');

function the_posts_preview_draft_posts($posts, $wp_query) {

    //abort if $posts is not empty, this query ain't for us...
    if ( count($posts) ) {
        return $posts;
    }

    $p = get_query_var('p');

    //get our post instead and return it as the result...
    if ( !empty($p) ) {
        return array(get_post($p));
    }
}

add_filter('the_posts', 'the_posts_preview_draft_posts', 10, 2);

function allow_draft_public_preview( ) {

        global $post;

        if ( !empty($post->post_name) )
            return;

        $html =<<<DOC
        <div class="misc-pub-section">
            <label>
                <input type="checkbox" id="_allow_public_preview" name="_allow_public_preview" value="1" />
                Allow public preview
            </label>
        </div>
DOC;

echo $html;

}

add_action('post_submitbox_misc_actions', 'allow_draft_public_preview');

function update_post_status( $post_id, $post, $update ) {

    if ( !empty($_POST['_allow_public_preview']) ) {
        
        //un-hook to prevent infinite loop
        remove_action( 'save_post', 'update_post_status', 13, 2 );

        //set the post to publish so it gets the slug is saved to post_name
        wp_update_post( array( 'ID' => $post_id, 'post_status' => 'publish' ) );

        //immediately put it back to draft status
        wp_update_post( array( 'ID' => $post_id, 'post_status' => 'draft' ) );

        //re-hool
        add_action( 'save_post', 'update_post_status', 13, 2 );
    }

}

add_action('save_post', 'update_post_status', 13, 2);
8
  • Thank you soooo much! This worked perfectly!! Which, in a way, I find strange - since I feel I was already there with a very similar approach (which I found at the source / link above). I've updated my post with the code I was using before, which did not work (gave me a 404 when I viewed a page as a non logged in user). T H A N K S again!!
    – Sebastian
    Commented Feb 20, 2016 at 17:47
  • Update: I just noticed a problem when viewing posts in status private. They should of course not be visible to non logged in users (that part works) - however, they are not visible anymore to logged in users!
    – Sebastian
    Commented Feb 20, 2016 at 18:29
  • 1
    Glad it works, to some degree. My example above is not without its caveats, one of which you probably found concerning private posts. It is however a means to get you started in the right direction. Likely we need some further logic to maintain private post functionality. If I get some time today I'll play around with it and try and reproduce the bug and work out a fix.
    – Adam
    Commented Feb 21, 2016 at 4:22
  • 1
    Is that once you have already saved it to published at least once? I imagine there would be several scenarions that we would need to cater for. You can probably hook into change_post_status instead of using a wpdb call directly. Interesting find though!
    – Adam
    Commented Feb 24, 2016 at 12:51
  • 1
    Yes, even when a post is published and then afterwards set to "pending" or "draft", my WP 4.2 removes the "post_name" value (slug) from the database for that post. So I am just setting this value via the direct database call (UPDATE wp_wms_posts SET ...) ;-) Thank you very much for your help - much appreciated!!
    – Sebastian
    Commented Feb 24, 2016 at 16:49

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.