How do I create a private, members only RSS feed for a custom post type so our registered users can use their own unique RSS feed URL to get content in their podcatcher? But no public RSS feed.
The Goal: A members only RSS feed for a custom post type (private podcast), that works in podcatchers. Think patreon private rss feed, but for WordPress. It’s a very similar concept.
Members get a unique URL or authenticate via the feed URL without exposing their account if someone else got their feed URL.
I need to be able to provide a private RSS feed to members of our site, using some sort of auth or uniqueness so only that user has the URL and access to the feed. (I will check their membership levels based on 3rd party membership plugins after, just authenticating the user is enough for now).
What I’ve done: I have created the custom post type, custom taxonomies, registered the new feed, added the RSS template, turned off all other feeds and have been playing with a number of ways to setup the authentication. This is where I get stuck!
At the moment, I have the content hidden using WC Memberships (but this could be any membership system, just slight changes in code for which one to check). Based on this, when a user is not logged in, the feed displays title only when they visit the feed URL.
I then added a layer of authentication to try and use the user credentials to authenticate… This hides the whole feed from public, until logged in through a popup login window (this popup is normal http auth like a password protected page, a JS alert maybe?), but then logging in still shows the WC memberships barrier unless you were logged in prior to visiting the feed URL. This is where I’ll have to run the 3rd part membership checks.
The problem: I need to generate unique feed URLS somehow to auth the user from a podcatcher without a login interface. Whether that URL is generated based on their auth status on our site prior to registering the feed URL or we append the user credentials as a query string (which seems like a security risk), I don’t know. But I need a single URL that gives a user with active membership access to this RSS feed. It is important that this access can be revoked somehow.
I feel like I’ve come very close a number of iterations through this. But maybe my approach is wrong.
Maybe I need to create a unique feed URL per user, so their unique feed is created when they sign up, but is removed when they are not active.
Maybe I need to have authentication set up so the URL has a query string or unique ID that logs them in. But I want to avoid these URLs allowing anyone with the URL to log in to a user’s account on the site.
I hope this makes sense and that you have some insight into the situation.
I have spent a lot of time trying every single thing I have found by searching authenticated RSS feed, private RSS. It feels like I am so close! Where is the missing piece of the puzzle?
Thanks in advance!
Here’s my code as it stands now:
<?php
/*
Plugin Name: Private Podcast
Plugin URI: #
Description: Adds a cpt "Podcast" to create a private, members only podcast.
Version: 1.0
Author: Benbodhi
Author URI: https://benbodhi.com
Textdomain: private_podcast
License: GPLv2
*/
defined( 'ABSPATH' ) || exit;
define( 'BODHI_PP_PLUGIN_PATH', plugin_dir_path( __FILE__ ) ); // define the absolute plugin path for includes
define( 'BODHI_PP_PLUGIN_URL', plugin_dir_url( __FILE__ ) ); // define the plugin url for use in enqueue
$plugin_file = plugin_basename(__FILE__); // plugin file for reference
/**
* Register Post Type
*/
if ( ! function_exists('bodhi_pp_podcast_post_type') ) {
function bodhi_pp_podcast_post_type() {
$labels = array(
'name' => _x( 'Private Podcast', 'Post Type General Name', 'hardcore_cpt' ),
'singular_name' => _x( 'Episode', 'Post Type Singular Name', 'hardcore_cpt' ),
'menu_name' => __( 'Private Podcast', 'hardcore_cpt' ),
'name_admin_bar' => __( 'Private Podcast', 'hardcore_cpt' ),
'archives' => __( 'Episode Archives', 'hardcore_cpt' ),
'attributes' => __( 'Episode Attributes', 'hardcore_cpt' ),
'parent_item_colon' => __( 'Parent Episode:', 'hardcore_cpt' ),
'all_items' => __( 'All Episodes', 'hardcore_cpt' ),
'add_new_item' => __( 'Add New Episode', 'hardcore_cpt' ),
'add_new' => __( 'Add New Episode', 'hardcore_cpt' ),
'new_item' => __( 'New Episode', 'hardcore_cpt' ),
'edit_item' => __( 'Edit Episode', 'hardcore_cpt' ),
'update_item' => __( 'Update Episode', 'hardcore_cpt' ),
'view_item' => __( 'View Episode', 'hardcore_cpt' ),
'view_items' => __( 'View Episodes', 'hardcore_cpt' ),
'search_items' => __( 'Search Episode', 'hardcore_cpt' ),
'not_found' => __( 'Not found', 'hardcore_cpt' ),
'not_found_in_trash' => __( 'Not found in Trash', 'hardcore_cpt' ),
'featured_image' => __( 'Featured Image', 'hardcore_cpt' ),
'set_featured_image' => __( 'Set featured image', 'hardcore_cpt' ),
'remove_featured_image' => __( 'Remove featured image', 'hardcore_cpt' ),
'use_featured_image' => __( 'Use as featured image', 'hardcore_cpt' ),
'insert_into_item' => __( 'Insert into episode', 'hardcore_cpt' ),
'uploaded_to_this_item' => __( 'Uploaded to this episode', 'hardcore_cpt' ),
'items_list' => __( 'Episode list', 'hardcore_cpt' ),
'items_list_navigation' => __( 'Episode list navigation', 'hardcore_cpt' ),
'filter_items_list' => __( 'Filter episode list', 'hardcore_cpt' ),
);
$args = array(
'label' => __( 'Episode', 'hardcore_cpt' ),
'description' => __( 'Episode Description', 'hardcore_cpt' ),
'labels' => $labels,
'supports' => array( 'title', 'editor', 'thumbnail' ),
'taxonomies' => array(),
'hierarchical' => false,
'public' => true,
'show_ui' => true,
'show_in_menu' => true,
'menu_position' => 5,
'menu_icon' => 'dashicons-microphone',
'show_in_admin_bar' => true,
'show_in_nav_menus' => true,
'can_export' => true,
'has_archive' => true,
'exclude_from_search' => true,
'publicly_queryable' => true,
'capability_type' => 'post',
'show_in_rest' => true,
);
register_post_type( 'private-podcast', $args );
}
add_action( 'init', 'bodhi_pp_podcast_post_type', 0 );
}
/**
* Register Categories Taxonomy
*/
if ( ! function_exists('bodhi_pp_tax_category') ) {
function bodhi_pp_tax_category() {
$labels = array(
'name' => _x( 'Podcast Categories', 'Taxonomy General Name', 'hardcore_cpt' ),
'singular_name' => _x( 'Podcast Category', 'Taxonomy Singular Name', 'hardcore_cpt' ),
'menu_name' => __( 'Podcast Categories', 'hardcore_cpt' ),
'all_items' => __( 'All Podcast Categories', 'hardcore_cpt' ),
'parent_item' => __( 'Parent Category', 'hardcore_cpt' ),
'parent_item_colon' => __( 'Parent Category:', 'hardcore_cpt' ),
'new_item_name' => __( 'New Podcast Category', 'hardcore_cpt' ),
'add_new_item' => __( 'Add New Podcast Category', 'hardcore_cpt' ),
'edit_item' => __( 'Edit Podcast Category', 'hardcore_cpt' ),
'update_item' => __( 'Update Podcast Category', 'hardcore_cpt' ),
'view_item' => __( 'View Podcast Category', 'hardcore_cpt' ),
'separate_items_with_commas' => __( 'Separate categories with commas', 'hardcore_cpt' ),
'add_or_remove_items' => __( 'Add or Remove Podcast Categories', 'hardcore_cpt' ),
'choose_from_most_used' => __( 'Choose from the most used', 'hardcore_cpt' ),
'popular_items' => __( 'Popular Podcast Categories', 'hardcore_cpt' ),
'search_items' => __( 'Search Podcast Categories', 'hardcore_cpt' ),
'not_found' => __( 'Not Found', 'hardcore_cpt' ),
'no_terms' => __( 'No Podcast Categories', 'hardcore_cpt' ),
'items_list' => __( 'Podcast Categories list', 'hardcore_cpt' ),
'items_list_navigation' => __( 'Podcast Categories list navigation', 'hardcore_cpt' ),
);
$args = array(
'labels' => $labels,
'hierarchical' => true,
'public' => true,
'show_ui' => true,
'show_admin_column' => true,
'show_in_nav_menus' => true,
'show_tagcloud' => false,
'show_in_rest' => true,
);
register_taxonomy( 'podcast_category', array( 'private-podcast' ), $args );
}
add_action( 'init', 'bodhi_pp_tax_category', 0 );
}
/**
* Register Tags Taxonomy
*/
if ( ! function_exists('bodhi_pp_tax_tag') ) {
function bodhi_pp_tax_tag() {
$labels = array(
'name' => _x( 'Podcast Tags', 'Taxonomy General Name', 'hardcore_cpt' ),
'singular_name' => _x( 'Podcast Tag', 'Taxonomy Singular Name', 'hardcore_cpt' ),
'menu_name' => __( 'Podcast Tags', 'hardcore_cpt' ),
'all_items' => __( 'All Podcast Tags', 'hardcore_cpt' ),
'parent_item' => __( 'Parent Item', 'hardcore_cpt' ),
'parent_item_colon' => __( 'Parent Item:', 'hardcore_cpt' ),
'new_item_name' => __( 'New Podcast Tag', 'hardcore_cpt' ),
'add_new_item' => __( 'Add New Podcast Tag', 'hardcore_cpt' ),
'edit_item' => __( 'Edit Podcast Tag', 'hardcore_cpt' ),
'update_item' => __( 'Update Podcast Tag', 'hardcore_cpt' ),
'view_item' => __( 'View Podcast Tag', 'hardcore_cpt' ),
'separate_items_with_commas' => __( 'Separate Podcast Tags with commas', 'hardcore_cpt' ),
'add_or_remove_items' => __( 'Add or remove Podcast Tags', 'hardcore_cpt' ),
'choose_from_most_used' => __( 'Choose from the most used', 'hardcore_cpt' ),
'popular_items' => __( 'Popular Podcast Tags', 'hardcore_cpt' ),
'search_items' => __( 'Search Podcast Tags', 'hardcore_cpt' ),
'not_found' => __( 'Not Found', 'hardcore_cpt' ),
'no_terms' => __( 'No Podcast Tags', 'hardcore_cpt' ),
'items_list' => __( 'Podcast Tag list', 'hardcore_cpt' ),
'items_list_navigation' => __( 'Podcast Tag list navigation', 'hardcore_cpt' ),
);
$args = array(
'labels' => $labels,
'hierarchical' => false,
'public' => true,
'show_ui' => true,
'show_admin_column' => true,
'show_in_nav_menus' => true,
'show_tagcloud' => true,
'show_in_rest' => true,
);
register_taxonomy( 'podcast_tag', array( 'private-podcast' ), $args );
}
add_action( 'init', 'bodhi_pp_tax_tag', 0 );
}
/**
* RSS Feed Auth
* ref: https://jerickson.net/requiring-authentication-wordpress-feeds/
*/
if ( ! function_exists('bodhi_pp_rss_auth') ) {
function bodhi_pp_rss_auth() {
if (!isset($_SERVER['PHP_AUTH_USER'])) {
header('WWW-Authenticate: Basic realm="RSS Feeds"');
header('HTTP/1.0 401 Unauthorized');
echo 'Feeds from this site are private';
exit;
} else {
if (is_wp_error(wp_authenticate($_SERVER['PHP_AUTH_USER'],$_SERVER['PHP_AUTH_PW']))) {
header('WWW-Authenticate: Basic realm="RSS Feeds"');
header('HTTP/1.0 401 Unauthorized');
echo 'Username and password were not correct';
exit;
}
}
}
add_action('do_feed_private-podcast', 'bodhi_pp_rss_auth', 1);
}
/**
* Register RSS Feed
*/
if ( ! function_exists('bodhi_pp_rss') ) {
function bodhi_pp_rss(){
add_feed('private-podcast', 'bodhi_pp_rss_template');
}
add_action('init', 'bodhi_pp_rss');
}
/**
* RSS Feed Template
*/
if ( ! function_exists('bodhi_pp_rss_template') ) {
function bodhi_pp_rss_template(){
require_once( BODHI_PP_PLUGIN_PATH . '/rss-template.php' );
}
}
/**
* Disable other RSS Feeds
*/
function bodhi_pp_disable_feed() {
// wp_die( __( 'No feed available, please visit the <a href="'. esc_url( home_url( '/' ) ) .'">homepage</a>!' ) );
wp_redirect(home_url());
exit();
}
add_action('do_feed', 'bodhi_pp_disable_feed', 1);
add_action('do_feed_rdf', 'bodhi_pp_disable_feed', 1);
add_action('do_feed_rss', 'bodhi_pp_disable_feed', 1);
add_action('do_feed_rss2', 'bodhi_pp_disable_feed', 1);
add_action('do_feed_atom', 'bodhi_pp_disable_feed', 1);
add_action('do_feed_rss2_comments', 'bodhi_pp_disable_feed', 1);
add_action('do_feed_atom_comments', 'bodhi_pp_disable_feed', 1);
// remove links from site head
remove_action( 'wp_head', 'feed_links_extra', 3 );
remove_action( 'wp_head', 'feed_links', 2 );
/**
* Set RSS Language
*/
function bodhi_pp_rss_lang_attr($output, $show) {
if ( $show == 'language' ) {
$output = 'en-US';
}
return $output;
}
add_filter('bloginfo_rss', 'bodhi_pp_rss_lang_attr', 10, 2);