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

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.

Read More

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;
        }
    }

Related posts

Leave a Reply

1 comment

  1. 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…