Allow users to create their own feed from selected categories?

This is a really cool idea.

I don’t think part 2 should be handled inside WordPress: there are plenty of RSS to email providers. They’re going to be way better at that than a plugin (or theme) is likely to be.

BUT we can create RSS feeds.

Step one: set up a class to wrap everything up.

There are a few class constants and variables here — we’ll use them later. Just a singleton pattern.

<?php
class Per_User_Feeds
{
    // Where we'll store the user cats
    const META_KEY = '_per_user_feeds_cats';

    // Nonce for the form fields
    const NONCE = '_user_user_feeds_nonce';

    // Taxonomy to use
    const TAX = 'category';

    // The query variable for the rewrite
    const Q_VAR = 'puf_feed';

    // container for the instance of this class
    private static $ins = null;

    // container for the terms allowed for this plugin
    private static $terms = null;

    public static function init()
    {
        add_action('plugins_loaded', array(__CLASS__, 'instance'));
    }

    public static function instance()
    {
        is_null(self::$ins) && self::$ins = new self;
        return self::$ins;
    }
}

Step two: add a field to the user profile pages (and save it)

You’ll need to hook into show_user_profile and edit_user_profile to do this. Spit out a nonce, a label, and the field. show_user_profile fires when users view their profile in the admin area. edit_user_profile fires when they edit another’s profile — this is how your admin user will go in an edit user’s categories.

<?php
class Per_User_Feeds
{
    // snip snip

    protected function __construct()
    {
        add_action('show_user_profile', array($this, 'field'));
        add_action('edit_user_profile', array($this, 'field'));
    }

    public function field($user)
    {
        wp_nonce_field(self::NONCE . $user->ID, self::NONCE, false);

        echo '<h4>', esc_html__('Feed Categories', 'per-user-feed'), '</h4>';

        if($terms = self::get_terms())
        {
            $val = self::get_user_terms($user->ID);
            printf('<select name="%1$s[]" id="%1$s" multiple="multiple">', esc_attr(self::META_KEY));
            echo '<option value="">', esc_html__('None', 'per-user-feed'), '</option>';
            foreach($terms as $t)
            {
                printf(
                    '<option value="%1$s" %3$s>%2$s</option>',
                    esc_attr($t->term_id),
                    esc_html($t->name),
                    in_array($t->term_id, $val) ? 'selected="selected"' : ''
                );
            }
            echo '</select>';
        }
    }
}

That also introduces our first two helper methods:

  1. get_user_terms, a simple wraper around get_user_meta with a call to apply_filters — let others modify things if they want!
  2. get_terms a wrapper around get_terms with a call to apply_filters.

Both of these are just convenience things. They also provide ways for other plugins/themes to hook in and modify things.

<?php
/**
 * Get the categories available for use with this plugin.
 *
 * @uses    get_terms
 * @uses    apply_filters
 * @return  array The categories for use
 */
public static function get_terms()
{
    if(is_null(self::$terms))
        self::$terms = get_terms(self::TAX, array('hide_empty' => false));

    return apply_filters('per_user_feeds_terms', self::$terms);
}

/**
 * Get the feed terms for a given user.
 *
 * @param   int $user_id The user for which to fetch terms
 * @uses    get_user_meta
 * @uses    apply_filters
 * @return  mixed The array of allowed term IDs or an empty string
 */
public static function get_user_terms($user_id)
{
    return apply_filters('per_user_feeds_user_terms',
        get_user_meta($user_id, self::META_KEY, true), $user_id);
}

To save the fields, hook into personal_options_update (fires when user save their own profile) and edit_user_profile_update (fires when saving another user’s profile).

<?php
class Per_User_Feeds
{
    // snip snip

    protected function __construct()
    {
        add_action('show_user_profile', array($this, 'field'));
        add_action('edit_user_profile', array($this, 'field'));
        add_action('personal_options_update', array($this, 'save'));
        add_action('edit_user_profile_update', array($this, 'save'));
    }

    // snip snip

    public function save($user_id)
    {
        if(
            !isset($_POST[self::NONCE]) ||
            !wp_verify_nonce($_POST[self::NONCE], self::NONCE . $user_id)
        ) return;

        if(!current_user_can('edit_user', $user_id))
            return;

        if(!empty($_POST[self::META_KEY]))
        {
            $allowed = array_map(function($t) {
                return $t->term_id;
            }, self::get_terms());

            // PHP > 5.3: Make sure the items are in our allowed terms.
            $res = array_filter(
                (array)$_POST[self::META_KEY],
                function($i) use ($allowed) {
                    return in_array($i, $allowed);
                }
            );

            update_user_meta($user_id, self::META_KEY, array_map('absint', $res));
        }
        else
        {
            delete_user_meta($user_id, self::META_KEY);
        }
    }
}

Step three: provide a feed

Since this is very much a custom feed, we don’t want to hijack something like author feeds to get this done (though that’s an option!). Instead let’s add a rewrite: yoursite.com/user-feed/{{user_id}} will render the personalized user feed.

To add the rewrite we need to hook into init and use add_rewrite_rule. Since this uses a custom query variable to detect when we’re on a personalized user feed, we also need to hook into query_vars and our our custom variable so WordPress doesn’t ignore it.

<?php
class Per_User_Feeds
{
    // snip snip

    protected function __construct()
    {
        add_action('show_user_profile', array($this, 'field'));
        add_action('edit_user_profile', array($this, 'field'));
        add_action('personal_options_update', array($this, 'save'));
        add_action('edit_user_profile_update', array($this, 'save'));
        add_action('init', array($this, 'rewrite'));
        add_filter('query_vars', array($this, 'query_var'));
    }

    // snip snip

    public function rewrite()
    {
        add_rewrite_rule(
            '^user-feed/(\d+)/?$',
            'index.php?' . self::Q_VAR . '=$matches[1]',
            'top'
        );
    }

    public function query_var($v)
    {
        $v[] = self::Q_VAR;
        return $v;
    }
}

To actually render the feed, we’ll hook into template_redirect, look for our custom query var (bailing if we don’t find it), and hijack the global $wp_query with a personalized version.

I also hooked into wp_title_rss to modify the RSS title, which was a bit weird: it grabbed the first category and displayed the feed title as if looking at a single category.

<?php
class Per_User_Feeds
{
    // snip snip

    protected function __construct()
    {
        add_action('show_user_profile', array($this, 'field'));
        add_action('edit_user_profile', array($this, 'field'));
        add_action('personal_options_update', array($this, 'save'));
        add_action('edit_user_profile_update', array($this, 'save'));
        add_action('init', array($this, 'rewrite'));
        add_filter('query_vars', array($this, 'query_var'));
        add_action('template_redirect', array($this, 'catch_feed'));
    }

    // snip snip

    public function catch_feed()
    {
        $user_id = get_query_var(self::Q_VAR);

        if(!$user_id)
            return;

        if($q = self::get_user_query($user_id))
        {
            global $wp_query;
            $wp_query = $q;

            // kind of lame: anon function on a filter...
            add_filter('wp_title_rss', function($title) use ($user_id) {
                $title=" - " . __('User Feed', 'per-user-feed');

                if($user = get_user_by('id', $user_id))
                    $title .= ': ' . $user->display_name;

                return $title;
            });
        }

        // maybe want to handle the "else" here?

        // see do_feed_rss2
        load_template( ABSPATH . WPINC . '/feed-rss2.php' );
        exit;
    }
}

To actually render the feed we rely on wp-includes/feed-rss2.php. You could replace this with something more custom, but why not be lazy?

There’s also a third helper method here: get_user_query. Same idea as the helpers above — abstract away some reusable functionality and provide hooks.

<?php
/**
 * Get a WP_Query object for a given user.
 *
 * @acces   public
 * @uses    WP_Query
 * @return  object WP_Query
 */
public static function get_user_query($user_id)
{
    $terms = self::get_user_terms($user_id);

    if(!$terms)
        return apply_filters('per_user_feeds_query_args', false, $terms, $user_id);

    $args = apply_filters('per_user_feeds_query_args', array(
        'tax_query' => array(
            array(
                'taxonomy'  => self::TAX,
                'terms'     => $terms,
                'field'     => 'id',
                'operator'  => 'IN',
            ),
        ),
    ), $terms, $user_id);

    return new WP_Query($args);
}

Here is all of the above as a plugin. The plugin (and subsequently, this answer) requires PHP 5.3+ due to the use of anonymous functions.

Leave a Comment