Take the 2-minute tour ×
WordPress Development Stack Exchange is a question and answer site for WordPress developers and administrators. It's 100% free, no registration required.

I have a page associated with a custom template that list all password protected posts.

Let's name it MyPage.

What I'm trying to achieve is when a user views MyPage to show only once the password protected form and when he/she enter a valid password -> to redirect back to MyPage so he can view the listing of the protected posts.

I want on the listing template to show only one form and when the user entered his password to show only the posts with the same password (or the posts in the same category) that are unlocked.

The password is the default post protection password from the publish meta box:

WordPress post publish meta box

share|improve this question
    
Do you need to have multiple passwords for the same page? Each password, corresponds to and shows only posts with the same password-protected password? –  userabuser Feb 21 at 14:15
    
First thanks kaiser for the edit. @userbuser - sorry my English is very bad and couldn't understand what exactly you are asking :(. Could you perephrase it more simple (sorry...) –  NoSense Feb 21 at 14:30
1  

3 Answers 3

up vote 6 down vote accepted

TL;TR? Spoiler alert - hover over the next blockquote to expose it.

Building an archive that is easily filterable by the currently entered password is nothing that is easy and surely not elegant buildable. The solution involves a secondary query and a small plugin, but it works. All errors and wrong paths are shown as well to help you avoid making the same mistakes.

DB Structure

The password is part of the main $wpdb->posts table. It's saved in plain text (just to make that clear. Don't be afraid of that. It's not connected to any actual user login, so you don't have to be afraid that you are opening a security hole when someone finds it per accident.

Building/Altering the main query

To reduce the results to password protected posts, we're intercepting the WHERE clause for:

  • A Page
  • Anything Public
  • All pages with a template that has a file name of password-protected.php

Here is the actual code as a plugin. Please note, that it will not work. Both is_page_template() and is_page() with a slug argument will retrieve their data from get_queried_object(). And the queried object is present when we already have retrieved the final query result.

<?php
/** Plugin Name: (#135443) Password Protected Posts Page Template Query */

add_action( 'pre_get_posts', 'wpse135443ExcludeProtectedAction' );
function wpse135443ExcludeProtectedAction( $query )
{
    if (
            is_page( 'slug-of-protected-page' )
            AND is_page_template( 'password-protected.php' )
            AND ! is_admin()
    )
        add_filter( 'posts_where', 'wpse135443ExcludeProtected' );
}
// Callback for the posts WHERE clause that only queries for password protected posts
function wpse135443ExcludeProtected( $where )
{
    return $where .= " AND NOT {$GLOBALS['wpdb']->posts}.post_password = '' ";
}

Page Template

There are several ways to go on this. One is using a filter:

add_filter( 'the_excerpt', 'wpse135443PasswordProtectedExcerpt' );
function wpse135443PasswordProtectedExcerpt( $excerpt )
{
    return post_password_required()
        ? get_the_password_form()
        : $excerpt;

}

Much easier would be to just list the title and then add the excerpt or content directly in the dedicated template. post_password_required() returns a boolean result so it's easy to check against it. The function accepts one argument, which is the post ID. It's already retrieved from the global inside the loop, but you can as well just use it outside the loop with throwing a password into it.

post_password_required() AND print get_the_password_form();

Retrieving the password and adding it to the query:

The following function adds the password to the (public) query string. As you can see this solution is quite unsecure: It exposes the password in the HTTP request.

add_action( 'login_form_postpass', 'wpse135443PasswordToQueryString' );
function wpse135443PasswordToQueryString()
{
    require_once ABSPATH . 'wp-includes/class-phpass.php';
    $hasher = new PasswordHash( 8, true );

    $expire = apply_filters( 'post_password_expires', time() + 10 * DAY_IN_SECONDS );
    setcookie( 'wp-postpass_' . COOKIEHASH, $hasher->HashPassword( wp_unslash( $_POST['post_password'] ) ), $expire, COOKIEPATH );

    exit( wp_safe_redirect( add_query_arg(
        'post_password',
        esc_attr( $_POST['post_password'] ),
        wp_get_referer()
    ) ) );
} );

While this might work, it's really not recommended to do that. If you really want to go from there, you'll hopefully know how to proceed. For the sake of sanity I'm not exposing more than this - snippet hunters might else grab and use it in themes for public distribution and I don't want to deliver the root of a problem.

Nothing works? Other options?

The first big problem is, that WordPress per default has no "Protected Posts Archive" as you can see in the template hierarchy. And when we want to alter the main query in a page template, we will have to check the SQL string and compare against it. Plugins might intercept that (poorly) and there's no reliable way to react with intercepted and altered SQL clauses in WP. An example of how the value of a filter callback/action might look, which is hooked to posts_clauses. $wpdb->posts is the name of your posts table.

array (size=7)
  'where' => string ' AND $wpdb->posts.ID = 1519 AND $wpdb->posts.post_type = 'page'' (length=63)
  'groupby' => string '' (length=0)
  'join' => string '' (length=0)
  'orderby' => string '$wpdb->posts.post_date DESC' (length=27)
  'distinct' => string '' (length=0)
  'fields' => string '$wpdb->posts.*' (length=14)
  'limits' => string '' (length=0)

Nothing to see here.

So the next thing we are trying is to add in another query. To make is easy to identify it, we will add an additional, custom query var.

$ppQuery = new WP_Query( array(
    'password_protected' => TRUE,
    'post_type'          => 'post',
    'posts_per_page'     => get_option( 'posts_per_page' ),
    'post_status'        => 'publish',
) );

The resulting SQL statement will look close to the following:

SELECT SQL_CALC_FOUND_ROWS  $wpdb->posts.*
FROM $wpdb->posts
WHERE 1=1
    AND $wpdb->posts.post_type = 'post' 
    AND ($wpdb->posts.post_status = 'publish')
    AND NOT ($wpdb->posts.post_password = '')
ORDER BY $wpdb->posts.post_date DESC LIMIT 0, 10

Then we can loop through it:

<?php
if ( $ppQuery->have_posts() )
{
    ?>
    <div class="entry-content">
        <?php echo get_the_password_form( $GLOBALS['post'] ); ?>
    </div>
    <?php
    while ( $ppQuery->have_posts() )
    {
        $ppQuery->the_post(); ?>

        <article id="post-<?php the_ID(); ?>" <?php post_class(); ?>>
            <?php the_title( '<header class="entry-header"><h2>', '</h2></header>' ); ?>
            <div class="entry-content">
            <?php
            post_password_required()
                OR the_excerpt();
            ?>
            </div>
        </article>

    <?php
    }
} ?>

The password form is shown at the top of the template. It takes the needed data from the global $post object, which is the first WP_Post instance held by the global $wp_query object. It doesn't really matter which post is used as this is only used to build the <label> for the form and the ID of the input field and nothing else. The form will still fire for name="post_password".

The excerpt is only shown if post_password_required() is TRUE. When firing the form, wp-login.php?action=postpass retrieves the $_POST data, sets the cookie and does a safe redirect. Therefore every post that shares the same password will be "unlocked" - WP is simply checking the Cookie Hash value against a hashed value of all the posts passwords. If it matches, it gets displayed.

Here is the plugin that aligns this template and that is needed to let it work.

<?php
/**
 * Plugin Name: (#135443) Password Protected Posts Page Template Query
 */

add_action( 'pre_get_posts', 'wpse135443ExcludeProtectedAction' );
function wpse135443ExcludeProtectedAction( $query )
{
    if (
        $query->get( 'password_protected' )
        AND ! is_admin()
    )
        add_filter( 'posts_where', 'wpse135443ExcludeProtected' );
}
// Callback for the posts WHERE clause that only queries for password protected posts
function wpse135443ExcludeProtected( $pieces )
{
    remove_filter( current_filter(), __FUNCTION__ );
    return $where .= " AND NOT ({$GLOBALS['wpdb']->posts}.post_password = '') ";
}

Filtering the posts

It would be nearly impossible (or only hardly achievable) to query against hashed values and to do hashing of post passwords during the query. So the easiest way is to filter the posts on runtime with a FilterIterator:

class PasswordProtectedPostsLoop extends \FilterIterator implements \Countable
{
    protected $wp_query;

    protected $allowed = FALSE;

    protected $total = 0;

    protected $counter = 0;

    /**
     * @param \Iterator $iterator
     * @param \WP_Query $wp_query
     */
    public function __construct( \Iterator $iterator, \WP_Query $wp_query )
    {
        # Setup properties
        // Global main query object
        NULL === $this->wp_query AND $this->wp_query = $wp_query;

        // Posts for this request
        $this->total = $this->wp_query->query_vars['posts_per_page'];

        // Internal counter
        0 !== $this->counter AND $this->counter = 0;

        $this->allowed = $this->wp_query->have_posts();

        parent::__construct( $iterator );
    }

    /**
     * @return bool
     */
    public function accept()
    {
        if (
            ! $this->allowed
            OR ! $this->current() instanceof \WP_Post
        )
            return FALSE;

        $this->wp_query->the_post();

        // Rewind posts for next loop
        $this->wp_query->current_post === $this->total -1
            AND $this->wp_query->rewind_posts();

        if ( ! post_password_required() )
            return FALSE;

        $this->counter++;
        return TRUE;
    }

    /**
     * Helper function to retrieve the ID of the currently looped post.
     * @return int
     */
    public function getID()
    {
        return $this->current()->ID;
    }

    /**
     * @return int
     */
    public function count()
    {
        return $this->counter;
    }
}

Looping would then be as simple as:

$ppArrayObj = new \ArrayObject( $ppQuery->->get_posts() );
$ppLoop = new PasswordProtectedPostsLoop( $ppArrayObj->getIterator(), $ppQuery );
$ppLoop->rewind();
foreach ( $ppyLoop as $post )
{
        ?>
        <article <?php post_class(); ?>>
            <?php the_title( '<header class="entry-header"><h2>', '</h2></header>' ); ?>
            <div class="entry-content">
                <?php the_excerpt(); >
            </div>
        </article>
        <?php
}
share|improve this answer
2  
OK. You win. :) –  s_ha_dum Feb 21 at 17:04
2  
@s_ha_dum Or in other words: Lost more than one hour on it... :) –  kaiser Feb 21 at 17:04
    
@kaiser - Thank you. With a little modification it works like a charm :)\ –  NoSense Feb 21 at 18:02

The future/next WP Version

...will already feature querying by has_password and by post_password. Trac ticket available.

share|improve this answer
    
It will be very useful feature :) –  NoSense Feb 21 at 18:04

Interesting question :)

Here is a very simple implementation.

/**
 * Template Name: Pass-Prot
 *
 */

function only_password_protected($where) {
  remove_action('posts_where','only_password_protected');
  return ' AND post_password != ""';
}
add_action('posts_where','only_password_protected');

$protected = new WP_Query(
  array(
    'post_type' => 'post'
  )
);
get_header();

if ($protected->have_posts()) {
  $form = true;
  while ($protected->have_posts()) {
    $protected->the_post();
    if (!post_password_required($post)) {
      $form = false;
      the_title();
      the_content();
      echo '<hr/>';
    }
  }
  if ($form) {
    echo get_the_password_form();
  }
}
get_footer();

That should work for simple cases. If you have a lot of password protected posts, or you need pagination, you will need something more complicated. You will probably need to check for post password cookies and have the query search for those specifically.

You will also need a way to "reset" to enter a new password.

Hopefully that gets you started.

share|improve this answer
    
I'm really appreciated for your help @s_ha_dum :) –  NoSense Feb 21 at 18:05

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

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