Filter get_adjacent_post() for private posts, how to modify JOIN/WHERE?

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…

Leave a Comment