Skip to content

Instantly share code, notes, and snippets.

@nacin
Created May 20, 2011 05:47
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save nacin/963d6e67a8b9736039f9 to your computer and use it in GitHub Desktop.
Save nacin/963d6e67a8b9736039f9 to your computer and use it in GitHub Desktop.
Subscribers Only
<?php
/*
* Plugin Name: Subscribers Only
* Description: Forces users to log in to view the site. Offers a per-user random key for use in feed readers. Also locks down uploads (which works on single-site only, running on Apache, under certain upload configurations).
* Author: Andrew Nacin
* Author URI: http://andrewnacin.com/
* Version: 0.2
*/
class Subscribers_Only {
static $instance;
const key_length = 32;
const meta_key = '_subscribers_only_feed_key';
const uploads_query_var = 'uploads_subscribers_only';
function __construct() {
self::$instance = $this;
add_action( 'init', array( $this, 'init' ) );
add_action( 'template_redirect', array( $this, 'template_redirect' ) );
add_filter( 'login_message', array( $this, 'login_message' ) );
add_action( 'tool_box', array( $this, 'tool_box' ) );
if ( is_multisite() )
return;
add_action( 'init', array( $this, 'add_external_rule' ), 11 );
register_activation_hook( __FILE__, array( $this, 'activate' ) );
register_deactivation_hook( __FILE__, array( $this, 'deactivate' ) );
}
function init() {
global $wp;
$wp->add_query_var( self::uploads_query_var );
}
function activate() {
$this->add_external_rule();
flush_rewrite_rules( true );
}
function deactivate() {
global $wp_rewrite;
// Yuck.
if ( false !== $key = array_search( 'index.php?' . self::uploads_query_var . '=$1', $wp_rewrite->non_wp_rules ) )
unset( $wp_rewrite->non_wp_rules[ $key ] );
flush_rewrite_rules( true );
}
function add_external_rule() {
global $wp_rewrite;
$upload_dir = wp_upload_dir();
// This probably isn't compatible with various upload directory configurations.
$relative_dir = str_replace( site_url( '/' ), '', $upload_dir['baseurl'] );
$wp_rewrite->add_external_rule( $relative_dir . '/(.*)', 'index.php?' . self::uploads_query_var . '=$1' );
}
function template_redirect() {
if ( is_feed() && ! is_user_logged_in() ) {
if ( $this->validate_feed_key() )
return;
else
wp_die( __( 'Sorry, this is a private site.', 'subscribers-only' ) );
}
if ( ! is_user_logged_in() ) {
$redirect = ( is_ssl() ? 'https://' : 'http://' ) . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
wp_redirect( wp_login_url( $redirect ) );
exit;
}
if ( apply_filters( 'subscribers_only_require_read', true ) && ! current_user_can( 'read' ) ) {
wp_die( __( 'Sorry, this is a private site.', 'subscribers-only' ) );
}
if ( ! is_feed()
&& ! is_multisite()
&& apply_filters( 'subscribers_only_maybe_serve_file', true )
&& $file = get_query_var( self::uploads_query_var )
)
$this->serve_file( $file );
}
function validate_feed_key() {
global $wpdb;
if ( empty( $_GET['key'] ) )
return false;
$key = preg_replace( '/[^a-z0-9]/i', '', $_GET['key'] );
if ( self::key_length != strlen( $key ) )
return false;
if ( $wpdb->get_var( $wpdb->prepare( "SELECT user_id FROM $wpdb->usermeta WHERE meta_key = %s AND meta_value = %s", $wpdb->prefix . self::meta_key, $key ) ) )
return true;
return false;
}
function login_message( $message ) {
return '<p class="message">' . __( 'You must log in to access this site.', 'subscribers-only' ) . '</p>';
}
function tool_box() {
$key = get_user_option( self::meta_key );
if ( isset( $_POST['generate-new-feed-key'] ) && isset( $_POST['_subscribers_only_nonce'] ) && wp_verify_nonce( $_POST['_subscribers_only_nonce'], 'generate-new-feed-key' ) ) {
$key = wp_generate_password( self::key_length, false, false );
update_user_option( get_current_user_id(), self::meta_key, $key );
}
echo '<div class="tool-box">';
echo '<h3 class="title">' . __( 'Secret Feed Key', 'subscribers-only' ) . '</h3>';
echo '<p>' . __( 'As this is a private site, you need to append a key to feeds for use in feed readers.' ) . '</p>';
if ( $key )
echo '<p><span class="description">' . sprintf( __( 'Example: %s', 'subscribers-only' ), add_query_arg( 'key', $key, get_feed_link() ) ) . '</p>';
echo '<p><input type="text" value="' . esc_attr( $key ) . '" class="regular-text" readonly="readonly" />';
echo '<form method="post">';
wp_nonce_field( 'generate-new-feed-key', '_subscribers_only_nonce' );
submit_button( __( 'Generate New Key', 'subscribers-only' ), 'secondary', 'generate-new-feed-key', false );
echo '</form></p></div>';
}
// Derived from wp-includes/ms-files.php.
function serve_file( $requested_file ) {
$upload_dir = wp_upload_dir();
$file = $upload_dir['basedir'] . '/' . $requested_file;
$file = apply_filters( 'subscribers_only_serve_file', $file );
if ( 0 !== validate_file( $requested_file ) || ! is_file( $file ) ) {
status_header( 404 );
die( '404 &#8212; File not found.' );
}
// We may override this later.
status_header( 200 );
// The rest comes from wp-includes/ms-files.php.
$mime = wp_check_filetype( $file );
if ( false === $mime[ 'type' ] && function_exists( 'mime_content_type' ) )
$mime[ 'type' ] = mime_content_type( $file );
if ( $mime[ 'type' ] )
$mimetype = $mime[ 'type' ];
else
$mimetype = 'image/' . substr( $file, strrpos( $file, '.' ) + 1 );
header( 'Content-Type: ' . $mimetype ); // always send this
header( 'Content-Length: ' . filesize( $file ) );
$last_modified = gmdate( 'D, d M Y H:i:s', filemtime( $file ) );
$etag = '"' . md5( $last_modified ) . '"';
header( "Last-Modified: $last_modified GMT" );
header( 'ETag: ' . $etag );
header( 'Expires: ' . gmdate( 'D, d M Y H:i:s', time() + 100000000 ) . ' GMT' );
// Support for Conditional GET
$client_etag = isset( $_SERVER['HTTP_IF_NONE_MATCH'] ) ? stripslashes( $_SERVER['HTTP_IF_NONE_MATCH'] ) : false;
if( ! isset( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) )
$_SERVER['HTTP_IF_MODIFIED_SINCE'] = false;
$client_last_modified = trim( $_SERVER['HTTP_IF_MODIFIED_SINCE'] );
// If string is empty, return 0. If not, attempt to parse into a timestamp
$client_modified_timestamp = $client_last_modified ? strtotime( $client_last_modified ) : 0;
// Make a timestamp for our most recent modification...
$modified_timestamp = strtotime($last_modified);
if ( ( $client_last_modified && $client_etag )
? ( ( $client_modified_timestamp >= $modified_timestamp) && ( $client_etag == $etag ) )
: ( ( $client_modified_timestamp >= $modified_timestamp) || ( $client_etag == $etag ) )
) {
status_header( 304 );
exit;
}
// If we made it this far, just serve the file
readfile( $file );
}
}
new Subscribers_Only;
@scribu
Copy link

scribu commented Jun 14, 2011

Mr. Nacin, what is self::$instance used for?

@stas
Copy link

stas commented Jun 14, 2011

Hmm, it's interesting actually.
You can use it later as a reference to your class object by accessing the static attribute $instance
Like:
MyClass::$instance->myMethod()

@scribu
Copy link

scribu commented Jun 14, 2011

Why would you do that, when you can just do MyClass::myMethod()?

You either use the singleton pattern all the way and make a static getInstance() method, or you just store a reference to the instance somewhere else.

@nacin
Copy link
Author

nacin commented Jun 14, 2011

It allows another plugin to interact with my hooks without me setting up a global that holds the instance. Not a full-blown singleton, just something written quickly. Example: remove_action( Subscribers_Only::$instance, 'tool_box' );

@scribu
Copy link

scribu commented Jun 14, 2011

That's why I use static methods:

add_action( 'template_redirect', array( __CLASS__, 'template_redirect' ) );

remove_action( 'template_redirect', array( 'Subscribers_Only', 'template_redirect' ) );

@xknown
Copy link

xknown commented Oct 6, 2011

I think there are some bugs.

  • The line 57 (global $wpdb;) should be moved inside the validate_feed_key function. There's a fatal error otherwise at line 94.
  • The query of validate_feed_key should be $wpdb->prepare( "SELECT user_id FROM $wpdb->usermeta WHERE meta_key = %s AND meta_value = %s", $wpdb->prefix . self::meta_key, $key ); -- user options are prefixed with $wpdb->prefix.
  • The function call validate_file( $file ) at line 130 should have a relative path as an argument. It may fail on Windows hosts or a custom upload directory that contains ./ or ../

If you are interested, you can pull these changes from https://gist.github.com/3ca0e7798e45f1ef20e3

@nacin
Copy link
Author

nacin commented Oct 6, 2011

Thanks, Alex!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment