5

Edit 3 - revised per G.M.'s suggestions. Code runs error free, but adjacent post filter still not filtering. Changes noted in code comments, current question noted at bottom of post.

Edit 2 - reflects G.M.'s code suggestion below. I'm confused about where the modified join/where filter function should go, so I added more of the mu-plugin code structure.

Edit 1 - reflects Kaiser's comment below, examples provided. Also, I was mistaken about which code was filtering my posts. Question does not relate to the Members plugin.

I have a mu-plugin filtering my WP query so that in the index loop, a visitor only sees posts they can read. It does this by offering post author the ability to assign a multi-level membership taxonomy for each post.

At the lowest (public) level, there is no membership taxonomy attached to the post.

Here is the query filter in the mu-plugin:

if ( ! class_exists ( 'Site_Members_Taxonomy' ) ) {

class Site_Members_Taxonomy
{
    //define( 'LANG', 'some_textdomain' ); deleted LANG, was throwing errors

    public $post_types = array( 'post', 'page', 'gallery' );
    public $tax = 'membership';
    public $tax_label; // CHANGE: made these three vars public

    public function __construct()
    {
    $this->tax_label = __( 'Membership' );

    // plugin defines taxonomy and membership meta and then:

    add_action( 'pre_get_posts', array( &$this, 'filter_query' ) );

    /**
     * Modify the query based on membership taxonomy
     */
    function filter_query( $query ) {

        //quit right now if in admin
        if( is_admin() ) return;

        $tax_query = '';
        $terms = $this->get_terms_to_exclude(); // see function below

        if( $terms ) 
            $tax_query = array(
                   array(
                        'taxonomy' => $this->tax,
                        'field' => 'slug',
                        'terms' => $terms,
                        'operator'=>'NOT IN'
                    )
            );

        // if after all that we have a tax query to make, lets do it!
        if ( $tax_query )
            set_query_var('tax_query', $tax_query);

    }

    } //end class

    global $Site_Members_Taxonomy;
    $Site_Members_Taxonomy = new Site_Members_Taxonomy();

}

// finally, outside the class definition, there is a function `members_can_view` to be used as a conditional to hide/show various template tags etc.

So far, so good. This filter works great.

The problem is, in single-post view, next_post_link() and previous_post_link() don't get the filter, so visitors can navigate to posts they cannot read. I want to filter the links to these posts to show the link only to the next post they can actually read.

I'd basically like to use the above query filter on get_adjacent_post().

In WP core trac for link-template.php (as well as in the post Kaiser linked to below) I found that next_post_link(), get_next_post() and get_adjacent_post() run the adjacent post query through the following filters:

  • get_{$adjacent}_post_join
  • get_{$adjacent}_post_where
  • get_{$adjacent}_post_sort

REWRITE JOIN/WHERE FILTER

I tried the following rewrite of JOIN/WHERE suggested in G.M.'s answer. As he says, it is very closely based on the wp-core function, with tr and tt given different names and the excluded terms passed directly to the function.

This function is currently active on the site and not throwing errors, however it's not filtering the adjacent posts query correctly either.

Current Question: When converting $excluded_terms from slugs to IDs in the function filter_adjacent below, am I correctly building the array? How can I log the excluded terms and find out what's going on?

    // Filters for get_adjacent_posts() so they don't show up in single-post navigation  
    // These filters are added within function __construct{} noted above
    // CHANGE: &$this to $this

    add_filter( 'get_previous_post_where', array( $this, 'filter_adjacent' ) );
    add_filter( 'get_next_post_where', array( $this, 'filter_adjacent' ) );
    add_filter( 'get_previous_post_join', array( $this, 'filter_adjacent' ) );
    add_filter( 'get_next_post_join', array( $this, 'filter_adjacent' ) );


    /**
     * These functions added within mu-plugin class:
     *
     * First, modify excluded terms based on membership taxonomy
     * 
     * NOTE: Excluded terms may be an array
     */

    function get_terms_to_exclude() {

        // if a real admin or "level1" we won't change tax query at all
        if( current_user_can ( 'manage_options' ) || current_user_can ( 'view_level1_posts' ) )
            return false;

        // default/public will exclude all upper level posts
        // in other words, all posts with membership taxonomy
        $terms = array( 'level1','level2','level3' );

        // if a level3 reader we'll exclude level1 and level2 posts
        if( current_user_can ( 'view_level3_posts' ) )
            $terms = array( 'level1', 'level2' );

        // if at level2 level we'll exclude level1 level posts
        if( current_user_can ( 'view_level2_posts' ) )
            $terms = array( 'level1' );

        return $terms;

    }


    /**
     * Next, use these terms to filter the get_adjacent_post JOIN/WHERE
     */

    function filter_adjacent( $clause ) {

        if ( substr_count( current_filter(), '_post_join' ) ) { // filtering join

            if ( empty($clause) ) $clause = '';
            global $wpdb;
            $clause .=
                " INNER JOIN {$wpdb->term_relationships} AS trmship ON p.ID = trmship.object_id
                INNER JOIN {$wpdb->term_taxonomy} ttmship
                ON trmship.term_taxonomy_id = ttmship.term_taxonomy_id";
            return $clause;

        } elseif ( substr_count( current_filter(), '_post_where' ) ) { // filtering where

            $excluded_term_slugs = get_terms_to_exclude();
            if ( ! $excluded_term_slugs ) return $clause; // nothing to filter if no terms
            $excluded_terms = array(); // we needs term ids, let's convert slug in terms ids
            foreach ( $excluded_term_slugs as $slug ) {
                $t = get_term_by( 'slug',  $slug, 'membership' );
                if ( ! $t || is_wp_error($t) ) continue;
                $excluded_terms[] = $t;
            }
            // something wrong in get_level_term_to_exclude()?
            if ( empty($excluded_terms) ) return $where; 

            $posts_in_ex_terms_sql = 
                " AND ttmship.taxonomy = 'membership'
                AND ttmship.term_id NOT IN (" . implode( $excluded_terms, ',' ) . ')';
            // return filtered where clause
            return $clause. $posts_in_ex_terms_sql;
        }
    }
2
  • As a starting point, please read this answer, then come back and file an edit with what you have tried and where you failed. Also please keep in mind that support for 3rd plugins and themes is out of scope. So take a look at how the plugin does hide the posts, copy the code into your question and explain the concept behind it.
    – kaiser
    Commented Apr 8, 2014 at 23:05
  • 1
    Thanks Kaiser. I've copied in code to show how the mu-plugin is filtering the main query, as well as the two ways i've tried to use the filters. I have read through the (very thorough!) answer you linked, and the WP function. Your example shows how to alter sort order, but altering the join or the where seems more involved. One problem is I just don't understand sql functions well enough.
    – nimmolo
    Commented Apr 12, 2014 at 8:08

1 Answer 1

3

First of all I suggest you to use a function to return the terms to exclude, that will help you to get them in different places without having to repeat code, e.g. in the 'pre_get_posts' filter and the adjacent post filters.

So:

function get_level_term_to_exclude() {
  if ( current_user_can ('manage_options') || current_user_can ('view_level1_posts') ) {
    return false;
  }
  $terms = array( 'level1', 'level2', 'level3' );
  if( current_user_can ( 'view_level2_posts' ) ) {
    $terms = array( 'level1' );
  }
  if( current_user_can ( 'view_level3_posts' ) ) {
    $terms = array( 'level1', 'level2' );
  }
  return $terms;
}

I removed comments for sake of semplicity, however you code is well commented and mine is taken from there. I just put the check for admins at function top.

After that, generally speacking, JOIN clause can't filter anything without WHERE, and WHERE will fail if using a table name not defined in JOIN, so does not exists a join method and a where method, but exists a join and where method.

In core, the filter for excluded terms is done both via 'WHERE' and JOIN, in fact at line #1183 you can see:

"WHERE p.post_date $op %s AND p.post_type = %s
 AND p.post_status = 'publish' $posts_in_ex_terms_sql"

inside the if ( ! empty( $excluded_terms ) ) { statement on line #1154

and on line #1141:

if ( $in_same_term || ! empty( $excluded_terms ) ) {
   $join = " INNER JOIN $wpdb->term_relationships AS tr ON p.ID = tr.object_id
        INNER JOIN $wpdb->term_taxonomy tt ON tr.term_taxonomy_id = tt.term_taxonomy_id";

The $posts_in_ex_terms_sql is set at line #1173 in this way:

$posts_in_ex_terms_sql = $wpdb->prepare(
  " AND tt.taxonomy = %s
   AND tt.term_id NOT IN (" . implode( $excluded_terms, ',' ) . ')', $taxonomy
);

I think that you can just copy code used by core when $excluded_terms is not empty... if it works for core should works also for you.

So you can mimic core workflow using "get_{$adjacent}_post_where" filter and "get_{$adjacent}_post_join".

So the function filter_adjacent should act on join and where clause, a simple check on current_filter can do the trick.

this is how your class should appear using my tips:

class Site_Members_Taxonomy {

  const LANG = 'some_textdomain';

  public $post_types = array( 'post', 'page', 'gallery' );

  public $tax = 'membership';

  public $tax_label;

  public function __construct() {
    $this->tax_label = __( 'Membership', self::LANG );
    add_action( 'pre_get_posts', array( $this, 'filter_query' ) );
    add_filter( 'get_previous_post_where', array( $this, 'filter_adjacent' ) );
    add_filter( 'get_next_post_where', array( $this, 'filter_adjacent' ) );
    add_filter( 'get_previous_post_join', array( $this, 'filter_adjacent' ) );
    add_filter( 'get_next_post_join', array( $this, 'filter_adjacent' ) );
  }

  function get_term_to_exclude() {
    if (
      is_admin()
      || current_user_can ('manage_options')
      || current_user_can ('view_level1_posts')
    ) {
      return false;
    }
    $terms = array( 'level1', 'level2', 'level3' );
    if( current_user_can ( 'view_level2_posts' ) ) {
      $terms = array( 'level1' );
    }
    if( current_user_can ( 'view_level3_posts' ) ) {
      $terms = array( 'level1', 'level2' );
    }
    return $terms;
  }

  /**
   * Modify the query based on membership taxonomy
   */
  function filter_query( $query ) {
    $terms = $this->get_term_to_exclude();
    if ( ! $terms ) return;
    $tax_query = array(
      array(
        'taxonomy' => $this->tax,
        'field' => 'slug',
        'terms' => $terms,
        'operator' => 'NOT IN'
      )
    );
    set_query_var( 'tax_query', $tax_query );
  }

  /**
   * Filter adjacent posts
   */
  function filter_adjacent( $clause ) {
    if ( substr_count( current_filter(), '_post_join' ) ) {
      if ( empty($clause) ) $clause = '';
      global $wpdb;
       $clause .=
        " INNER JOIN {$wpdb->term_relationships} trmship ON p.ID = trmship.object_id
        INNER JOIN {$wpdb->term_taxonomy} ttmship
        ON trmship.term_taxonomy_id = ttmship.term_taxonomy_id";
      return $clause;
    } elseif ( substr_count( current_filter(), '_post_where' ) ) {
      $excluded_term_slugs = $this->get_term_to_exclude();
      if ( ! $excluded_term_slugs ) return $clause;
      $excluded_terms = array();
      foreach ( $excluded_term_slugs as $slug ) {
        $t = get_term_by( 'slug',  $slug, $this->tax );
        if ( ! $t || is_wp_error($t) ) continue;
        $excluded_terms[] = $t->term_id;
      }
      $excluded_terms = array_filter( array_map( 'intval', $excluded_terms ) );
      if ( empty( $excluded_terms ) ) return $clause; 
      global $wpdb;
      $posts_in_ex_terms_sql = $wpdb->prepare(
        " AND ttmship.taxonomy = '%s'
        AND trmship.term_taxonomy_id NOT IN (" . implode( ',', $excluded_terms ) . ')',
        $this->tax
      );
      return $clause. $posts_in_ex_terms_sql;
    }
  }
}

I've used a different alias for taxonomy tables in this way the filter will work even if when get_adjacent_post is called any terms are passed to for $excluded_terms argument and /or $in_same_term is set to true.

Code is completely untested, let me know if it works...

5
  • Thanks G.M. It's not throwing an error now but it's still not filtering the tax in get_adjacent_post, i'm thinking of how i can test what it's doing. I've also checked the main query and the get_term_to_exclude() function is working, in the reformat you have here, splitting it out of filter_query. in other words filter_query is filtering pre_get_posts as before. I wonder if the problem is near the bottom in AND ttmship.taxonomy = '%s' - does it know what %s is? Should I hard-code the ttmship.taxonomy there?
    – nimmolo
    Commented Apr 17, 2014 at 6:55
  • @andrnimm posts have only one term in membership taxonomy or have more than one? If answer is more than one than filter will fail... because there is not a 'GROUP BY' clause in the sql. You can force posts to have only one term (the low priviledged one) or you have to modify the SQL... including a group by clause or using a sub query inside where clause.
    – gmazzap
    Commented Apr 17, 2014 at 7:03
  • @andrnimm I see $posts_in_ex_terms_sql = $wpdb->prepare(" AND ttmship.taxonomy = '%s' AND trmship.term_taxonomy_id NOT IN (" . implode( ',', $excluded_terms ) . ')', $this->tax ); ... taxonomy is not hardcoded...
    – gmazzap
    Commented Apr 17, 2014 at 7:05
  • @GM re: your question above, posts for sure have only one membership taxonomy (or none). However, $excluded_terms will usually be an array. I'm wondering if there may be a problem in the foreach loop converting $excluded_term_slugs to IDs.
    – nimmolo
    Commented May 1, 2014 at 6:47
  • @andrnimm conversion is dome via core get_term_by, I don't think it should be a problem.
    – gmazzap
    Commented May 1, 2014 at 13: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.