Extending WP-Members plugin: hiding private posts from non-logged in users

March 6th, 2012


About the Author: Dan

I’m Dan Smart, a 38 year old website developer, based in Swansea, UK. I have worked in the software development industry for over 15 years, with experience in web development, mobile handset development, and mobile networks. I work both on websites and web applications with systems such as Wordpress, Laravel, Backbone, Angular.js, node.js and mobile app development with PhoneGap, iOS, and Android. When I’m not developing websites and software, I am a keen runner, involved with mime performance group Innovo Physical Theatre, and also actively involved in my local church.

* Follow me on Twitter or contact me


I’ve been using the excellent WP-Members plugin, to extend the Swansea Community Church website, and have been pleased with its ability to offer all the membership/registration functionality that I need to add a membership section.

However there a couple of elements that I wanted to add to it to focus it for my needs.

  1. Firstly, to set a post as public or private (overriding the default ‘block all’ or ‘allow all’ settings), I needed to add a custom field to that post. This is fine for me as a developer, but as the admin of the site is not a developer, I wanted to make it more user friendly.
  2. Secondly, I want to be able to remove private posts from the loop (and this covers posts, podcasts, and things like the Recent Posts plugin, etc) when a visitor is not logged into the website, but then show everything to the logged-in user.

This blog article is the second of two articles. Find the first here: Extending WP-Members plugin: Add meta box for public/private

Hiding ‘blocked’ posts

The WP-Members plugin marks private posts as ‘blocked’ with a custom field of ‘block’ having the value ‘1’ or true. We need a way to stop listing posts with this custom field.

There are a number of different places blocked posts are currently listed:

  • Blog / category pages (the main ‘loop’)
  • Listing widgets, e.g. Recent Posts plugin
  • Other post types, e.g. Podcasts, Events (e.g. when events are added via The Events Calendar plugin, they are stored as an Events post type)

To go through each plugin and rewrite each to no longer include the post meta (custom field) of ‘block’ would be a laborious process, and is likely to not only introduce bugs, but also will be a maintenance nightmare: each time a plugin is updated you’d have to check whether the code is compatible, and modify appropriately, etc.

Thankfully, we can filter the query with a WordPress query filter hook, which will affect every call using a standard query. Note that this will not affect get_posts by standard unfortunately (the ‘suppress_filters’ is turned off by default) and so you may have to do some testing to see which areas of your site are affected.

The ‘posts_where’ filter

There are filters for the different part of the posts query, however we want to limit posts to those ‘where the post doesn’t have the block post meta set’. Therefore we will use the ‘posts_where’ filter, filtering the ‘WHERE’ clause of the SQL query.

add_filter('posts_where', 'hide_private_posts_from_guests');
  1. /**
  2.  * Hide posts (of any post type) from the loop when user not logged in
  3.  */
  4. function hide_private_posts_from_guests($where)
  5. {
  6.     // don't hide from admin
  7.     if( is_admin() ) return $where;
  8.  
  9.     // if user isn't logged in
  10.     if ( !is_user_logged_in() )
  11.     {
  12.       // we will do our filtering here
  13.     }
  14.  
  15.     return $where;
  16. }

In our filter, we call our function ‘hide_private_posts_from_guests’. We first check we’re not in the admin – we don’t want to limit posts from admin users (this is kind of a redundant check because of is_user_logged_in() but it doesn’t hurt to be explicit sometimes). We then only want to filter posts for users who are not logged in, thus only showing all posts (leaving the query as it is) for users who are logged in.

We then return the $where statement (that will be modified shortly) – as this is a filter we always return a value.

How to select posts that don’t have the ‘block’ meta data set

Although WP_Query is great (check out the parameters here), There unfortunately isn’t the ability in the meta queries (see ‘Custom Field Parameters’ in the Codex page above) to query posts that don’t have a meta field – you can query those posts that do have a meta field, but not the other way round.

Therefore we’ll have to get our hands dirty with some SQL. The way to achieve this is to use a subquery. It would be preferable to use a Join but this isn’t as straightforward in this case.

Here’s all our subquery:

SELECT $wpdb->posts.ID
  1. FROM $wpdb->posts
  2. INNER JOIN $wpdb->postmeta ON ($wpdb->posts.ID = $wpdb->postmeta.post_id)
  3. WHERE $wpdb->postmeta.meta_key = 'block'

This SQL query returns a list of post IDs of posts that have a postmeta key of ‘block’ (this requires an INNER JOIN to the postmeta table).

Update 11th November 2016 – WP Members no longer calls the meta key ‘block’ – see Wolf’s comment

The posts_where filter extends the ‘WHERE’ clause, to select any post IDs not in a list of post IDs that have the ‘block’ key. Our subquery is then returning the second part of this.

Here’s how we extend the WHERE statement to un-include those posts that are returned from our subquery:

global $wpdb;
  1. // subquery to select all posts that are explicitly 'blocked', then make sure we don't
  2. // include those in the main query's results
  3. $where .= " AND ($wpdb->posts.ID NOT IN
  4. (SELECT $wpdb->posts.ID
  5. FROM $wpdb->posts
  6. INNER JOIN $wpdb->postmeta ON ($wpdb->posts.ID = $wpdb->postmeta.post_id)
  7. WHERE $wpdb->postmeta.meta_key = 'block'
  8. )
  9. ) ";

We’re using the NOT IN SQL operator to reduce our list down to the posts we want.

Our total code is as below:

add_filter('posts_where', 'hide_private_posts_from_guests');
  1. /**
  2.  * Hide posts (of any post type) from the loop when user not logged in
  3.  */
  4. function hide_private_posts_from_guests($where)
  5. {
  6.  // don't hide from admin
  7.     if( is_admin() ) return $where;
  8.  
  9.  // if user isn't logged in
  10.  if ( !is_user_logged_in() )
  11.  {
  12.      global $wpdb;
  13.      // subquery to select all posts that are explicitly 'blocked', then make sure we don't
  14.      // include those in the main query's results
  15.      $where .= " AND ($wpdb->posts.ID NOT IN
  16.          (SELECT $wpdb->posts.ID
  17.           FROM $wpdb->posts
  18.           INNER JOIN $wpdb->postmeta ON ($wpdb->posts.ID = $wpdb->postmeta.post_id)
  19.           WHERE $wpdb->postmeta.meta_key = 'block'
  20.          )
  21.         ) ";
  22.     }
  23.  
  24.     return $where;
  25. }

Thoughts

The weakness of this code is that it has to run the post subquery which can return a pretty large results set when you have a lot of posts (so consider the scalability here), however we are limited by the tools that we have. It would be great if the WordPress API included a ‘not having meta key’ meta query.

Can you come up with a better alternative?


Switch to mobile version