-
-
Save nacin/963d6e67a8b9736039f9 to your computer and use it in GitHub Desktop.
<?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; |
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()
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.
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' );
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!
Mr. Nacin, what is
self::$instance
used for?