-
-
Save nacin/963d6e67a8b9736039f9 to your computer and use it in GitHub Desktop.
Subscribers Only
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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 — 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; |
That's why I use static methods:
add_action( 'template_redirect', array( __CLASS__, 'template_redirect' ) );
remove_action( 'template_redirect', array( 'Subscribers_Only', 'template_redirect' ) );
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
Thanks, Alex!
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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' );