Super Plugin Sale! Your Choice: BOGO or 30% Off »
Web Dev + WordPress + Security

s2member notes

I use s2member (free version) and s2member Pro on a few of my sites. Have been for several years now. Over the course of time, I have amassed a healthy collection of notes, code snippets and techniques for customizing default functionality, adding features, and so forth. Gonna post the collection online for the benefit of any others who may be seeking for similar modifications and/or related information.

Note: I am not affiliated with s2member in any way, nor do I consider myself an expert in anything s2-related. Just like to use their plugin and want to share some potentially useful code snippets. Hopefully they will be useful to others as well. And of course, if you are some kind of s2member guru and want to clarify or improve any of these examples, feel free to leave a comment. Thanks.

Selective loading of s2 CSS and JavaScript

When it comes to including s2 CSS and JavaScript, there are two basic modes: Lazy Loading “on” or Lazy Loading “off”. Lazy loading is quite effective in terms of preventing s2member from including CSS/JS files on every page even when not necessary. But I have found situations where more granular control is needed. Here are some code snippets I’ve used to fine-tune exactly where s2 includes its CSS and JavaScript files.

Disable s2 CSS on all pages

// disable s2 CSS on all pages
function disable_all_s2_css() {
add_action('ws_plugin__s2member_during_add_css', 'disable_all_s2_css'); 

Disable s2 JS only on store page, disable s2 CSS everywhere

// disable s2 JS only on store page, disable s2 CSS everywhere
function load_selectiv_s2_css_js() {
	if (!is_page('store')) {
		remove_action('wp_print_scripts', 'c_ws_plugin__s2member_css_js_themes::add_js_w_globals');
	remove_action('wp_print_styles', 'c_ws_plugin__s2member_css_js_themes::add_css');
add_action('wp', 'load_selectiv_s2_css_js');

Disable s2 CSS/JS from all pages except login/register page

// disable s2 CSS/JS from all pages except login/register page
function disable_most_s2_css_js() {
	if (in_array($GLOBALS['pagenow'], array('wp-login.php', 'wp-register.php'))) {
	remove_action('wp_print_styles', 'c_ws_plugin__s2member_css_js_themes::add_css');
	remove_action('wp_print_scripts', 'c_ws_plugin__s2member_css_js_themes::add_js_w_globals');
add_action('wp', 'disable_most_s2_css_js');

To use any of these functions, add it to your s2member hacks file, which is a must-use plugin located in /wp-content/mu-plugins/s2-hacks.php.

Customize the s2member upgrade email

When a customer upgrades their account, s2member will automatically send out an “upgrade” email, which can be customized using the following code:

// customize s2 upgrade email
function my_s2_modification_msg($s2member_default_msg, $vars = array()) {
	return "Thank you! You've been updated to:\n" . $vars["paypal"]["item_name"] . "\n\nVisit the Members Area to log in and download your items:";
add_filter("ws_plugin__s2member_modification_email_msg", "my_s2_modification_msg", 10, 2);

Add this to /wp-content/mu-plugins/s2-hacks.php and edit the return line with your custom message. Notice that the function accepts two parameters, $s2member_default_msg and $vars, that contain the default email message and post vars, respectively. All kinds of useful data contained in the $vars variable.

Force lazy-load of CSS and JavaScript

When s2member shortcodes are called via do_shortcode(), the plugin’s lazy-load functionality may have problems and not include the required JS/CSS on the necessary pages. To resolve, here is a snippet that can be added to the s2 hacks file, /wp-content/mu-plugins/s2-hacks.php:

// force lazy load CSS/JS
add_filter('ws_plugin__s2member_lazy_load_css_js', '__return_true');

Note that this is an older technique that may not be necessary now that s2member includes a setting to enable/disable the lazy loading of scripts and styles.

s2member .htaccess rules

s2member includes a bunch of directories that should be secured from all external access and/or protected against directory views and so forth. For reference, here is a summary of the .htaccess rules that are added automatically by s2member, along with the name of the directory for which they should be applied.

# directory: /s2member/
Options -Indexes
<IfModule dir_module>
	DirectoryIndex disabled
	DirectoryIndex index.php

# directory: /s2member-files/
Deny from all

# directory: /s2member-logs/
<IfModule authz_core_module>
	Require all denied
<IfModule !authz_core_module>
	deny from all

# Disallow directory indexing here.
Options -Indexes

# directory: /s2member-pro/
Options -Indexes
<IfModule dir_module>
	DirectoryIndex disabled
	DirectoryIndex index.php

Just to reiterate, these rules should be placed (or already exist) in the specified directories. They should NOT all be placed in the same directory.

Note about PayPal post requests

This is a note that I made to myself about seeing both of the following notification requests in the access logs:

POST /wp/?s2member_paypal_notify=1
POST /?s2member_paypal_notify=1

Basically be aware that s2member may for unknown reasons use either of these URIs (one with the WP install directory and the other without) when making PayPal notification requests. I spent several hours trying to figure out where/how/why s2 uses one or the other, but ultimately could not resolve. Finally gave up and now simply redirect all /wp/ (WP install directory) requests to root / via .htaccess.

Useful URLs for testing stuff

Testing any shopping cart plugin can get real hairy real quick. So I keep a log of all URLs that prove useful during testing and troubleshooting any s2member stuffz.

Hopefully won’t need to use any of these again, but you never know. Having them here will save me some time should I need to open or reopen any mysterious cases.

Add user meta fields

Here is a way to add all of the following as user meta fields upon payment (s2 payment notification):

List of all meta fields:






These fields may be used to create invoices, as illustrated in the next code example.

s2member invoice functions

Here are two functions that add payment info as new meta fields, as used for invoice creation and similar functionality, anywhere user data may be required.

// s2_signup_notification=true&user_id=17&item_number=2
// s2_signup_notification=true&user_id=%%user_id%%&item_number=%%item_number%%

function s2_payment_notification() {
	if(!empty($_GET['s2_signup_notification'])) {
		if(!empty($_GET['user_id']) && !empty($_GET['item_number'])) {

			$user_id = (integer) $_GET['user_id'];
			$item_number = (string) $_GET['item_number'];

			//global $current_user; get_currentuserinfo($user_id);
			$user = new WP_User($user_id);

			$user_login = get_user_field ("user_login", $user_id);
			$user_email = get_user_field ("user_email", $user_id);
			$first_name = get_user_field ("first_name", $user_id);
			$last_name = get_user_field ("last_name", $user_id);
			$full_name = get_user_field ("full_name", $user_id);
			$display_name = get_user_field ("display_name", $user_id);

			$s2member_custom = get_user_field ("s2member_custom", $user_id);
			$s2member_subscr_id = get_user_field ("s2member_subscr_id", $user_id);

			$s2member_subscr_or_wp_id = get_user_meta($user_id, 'user_id', false); // get_user_field ("s2member_subscr_or_wp_id", $user_id);
			// $wp_password = get_user_meta($user_id, 'user_pass');
			$wp_user_url = get_user_meta($user_id, 'user_url', false);

			$s2member_subscr_gateway = get_user_field ("s2member_subscr_gateway", $user_id);
			$s2member_registration_ip = get_user_field ("s2member_registration_ip", $user_id);
			$s2member_custom_fields = get_user_field ("s2member_custom_fields", $user_id);
			$s2member_file_download_access_log = get_user_field ("s2member_file_download_access_log", $user_id); 
			$s2member_file_download_access_arc = get_user_field ("s2member_file_download_access_arc", $user_id);
			$s2member_auto_eot_time = get_user_field ("s2member_auto_eot_time", $user_id);
			$s2member_last_payment_time = get_user_field ("s2member_last_payment_time", $user_id);
			$s2member_paid_registration_times = get_user_field ("s2member_paid_registration_times", $user_id);
			$s2member_first_payment_txn_id = get_user_option('s2member_first_payment_txn_id', $user_id);
			$s2member_access_role = get_user_field ("s2member_access_role", $user_id);
			$s2member_access_level = get_user_field ("s2member_access_level", $user_id);
			$s2member_access_label = get_user_field ("s2member_access_label", $user_id);
			$s2member_access_ccaps = get_user_field ("s2member_access_ccaps", $user_id);
			$s2member_login_counter = get_user_field ("s2member_login_counter", $user_id);

			//$wp_user_url = implode(', ', $wp_user_url);
			$s2member_custom_fields = implode(', ', $s2member_custom_fields);
			$s2member_file_download_access_log = implode(', ', $s2member_file_download_access_log);
			$s2member_file_download_access_arc = implode(', ', $s2member_file_download_access_arc);
			$s2member_paid_registration_times = implode(' - ', $s2member_paid_registration_times);
			$s2member_access_ccaps = implode(', ', $s2member_access_ccaps);

			$invoice  = 'User ID: ' . $user_id . "\n";
			$invoice .= 'Item #: ' . $item_number . "\n";
			$invoice .= 'Username: ' . $user_login . "\n";
			$invoice .= 'Email: ' . $user_email . "\n";
			$invoice .= 'First name: ' . $first_name . "\n";
			$invoice .= 'Last name: ' . $last_name . "\n"; 
			$invoice .= 'Full name: ' . $full_name . "\n";
			$invoice .= 'Display name: ' . $display_name . "\n";
			$invoice .= 'Custom value: ' . $s2member_custom . "\n";
			$invoice .= 'Subscriber ID: ' . $s2member_subscr_id . "\n";
			$invoice .= 'WordPress ID: ' . $s2member_subscr_or_wp_id . "\n";
			//$invoice .= 'Password: ' . $wp_password . "\n";
			$invoice .= 'User URL: ' . $wp_user_url . "\n";
			$invoice .= 'Gateway code: ' . $s2member_subscr_gateway . "\n";
			$invoice .= 'Registration IP: ' . $s2member_registration_ip . "\n";
			$invoice .= 'Custom Fields: ' . $s2member_custom_fields . "\n";
			$invoice .= 'File downloads: ' . $s2member_file_download_access_log . "\n";
			$invoice .= 'All downloads: ' . $s2member_file_download_access_arc . "\n";
			$invoice .= 'EOT time: ' . $s2member_auto_eot_time . "\n";
			$invoice .= 'Last payment: ' . $s2member_last_payment_time . "\n";
			$invoice .= 'Registration time: ' . $s2member_paid_registration_times . "\n";
			$invoice .= 'Payment ID: ' . $s2member_first_payment_txn_id . "\n";
			$invoice .= 'WordPress role: ' . $s2member_access_role . "\n";
			$invoice .= 'Access level: ' . $s2member_access_level . "\n";
			$invoice .= 's2 access: ' . $s2member_access_label . "\n";
			$invoice .= 'Capabilities: ' . $s2member_access_ccaps . "\n";
			$invoice .= 'Payment ID: ' . $s2member_login_counter . "\n";

			// do whatever with $invoice
add_action('init', 's2_payment_notification');

Here is the outer logic of that previous snippet, ready for custom functionality:

// s2_payment_notification=yes&user_id=%%user_id%%&item_number=%%item_number%%

function s2_payment_notification() {
	if(!empty($_GET['s2_payment_notification'])) {
		if(!empty($_GET['user_id']) && !empty($_GET['item_number'])) {
			// do stuff for signup notifications
add_action('init', 's2_payment_notification');

And here is yet another example that may be useful when working with s2member invoice stuff:

// s2_signup_notification=true&user_id=%%user_id%%&item_number=%%item_number%%

function s2_payment_notification() {
	if(!empty($_GET['s2_signup_notification'])) {
		if(!empty($_GET['user_id']) && !empty($_GET['item_number'])) {

			$user_id = (integer)$_GET['user_id'];
			$item_number = (string)$_GET['item_number'];
			$user = new WP_User($user_id);

			$first_name = $user->first_name;
			$last_name = $user->last_name;
			$email = $user->user_email;
			$username = $user->user_login;

			$s2member_subscr_id = get_user_option('s2member_subscr_id', $user_id);
			$s2member_custom_fields = get_user_option('s2member_custom_fields', $user_id);
			$s2member_custom = get_user_option('s2member_custom', $user_id);
			$s2member_registration_ip = get_user_option('s2member_registration_ip', $user_id);
			$s2member_paid_registration_times = get_user_option('s2member_paid_registration_times', $user_id);
			$s2member_first_payment_txn_id = get_user_option('s2member_first_payment_txn_id', $user_id);
			$s2member_last_payment_time = get_user_option('s2member_last_payment_time', $user_id);
			$s2member_auto_eot_time = get_user_option('s2member_auto_eot_time', $user_id);
			$s2member_file_download_access_log = get_user_option('s2member_file_download_access_log', $user_id);
			file_put_contents(WP_CONTENT_DIR.'/plugins/s2member-logs/my.log', 'Payment Notification Received for User ID: '.$user_id."\n", FILE_APPEND);

add_action('init', 's2_payment_notification');

And lastly, a few code snippets that have proven useful during invoice development:

$user_id = (integer) $_GET['user_id'];
$user = new WP_User($user_id);

$string = $_SERVER['QUERY_STRING'];
$string = sanitize_text_field($string);

parse_str($string, $vars);
foreach($vars as $var) {
	add_user_meta($user_id, 's2x_' . $var, $var, true);

foreach ($myOptions as $option => $value) {
	echo $option . ' = ' . $value . "<br />";

It’s a shame that s2member doesn’t include some sort of built-in invoicing functionality (even for the Pro version). I’ve spent waaaaay too long learning the s2member API and developing my own invoice solutions. I’m sure many others have as well! :P

s2member post variables

This snippet displays all s2member post variables (at the time, they may have changed so be advised):

echo 'txn_type ' . $vars['txn_type'];
echo 'txn_id ' . $vars['txn_id'];
echo 'custom ' . $vars['custom'];
echo 'mc_gross ' . $vars['mc_gross'];
echo 'mc_currency ' . $vars['mc_currency'];
echo 'tax ' . $vars['tax'];
echo 'payer_email ' . $vars['payer_email'];
echo 'first_name ' . $vars['first_name'];
echo 'last_name ' . $vars['last_name'];
echo 'option_name1 ' . $vars['option_name1'];
echo 'option_selection1 ' . $vars['option_selection1'];
echo 'option_name2 ' . $vars['option_name2'];
echo 'option_selection2 ' . $vars['option_selection2'];
echo 'item_name ' . $vars['item_name'];
echo 'item_number ' . $vars['item_number'];
echo 'proxy_verified ' . $vars['proxy_verified'];
echo 'subscr_gateway ' . $vars['subscr_gateway'];
echo 'subscr_id ' . $vars['subscr_id'];
echo 'eotper ' . $vars['eotper'];
echo 'ccaps ' . $vars['ccaps'];
echo 'level ' . $vars['level'];
echo 'ip ' . $vars['ip'];
echo 'period1 ' . $vars['period1'];
echo 'mc_amount1 ' . $vars['mc_amount1'];
echo 'period3 ' . $vars['period3'];
echo 'mc_amount3 ' . $vars['mc_amount3'];
echo 'initial_term ' . $vars['initial_term'];
echo 'initial ' . $vars['initial'];
echo 'regular ' . $vars['regular'];
echo 'regular_term ' . $vars['regular_term'];
echo 'recurring ' . $vars['recurring'];

I found this useful in determining which variables were doing what during a custom implementation of s2member.

s2member IPN signup vars

Here is a list of all s2member signup variables, via s2member_ipn_signup_vars:


Again, things tend to change a lot with s2member, so refer to official sources for current most accurate infos.

Basic invoice template

Similar to the invoice examples provided previously, here is another stab at putting together something that works:

function s2_user_invoice($user_id) {
	if (!empty($_GET['user_id'])) {
		$vars = get_user_meta($user_id, 'wptao_wp_s2member_ipn_signup_vars', true);
		if (strpos($vars['ccaps'], 'book') !== false) {
			$purchase_item = 'Tao of WordPress [PDF Book + Theme]';
		} else if (strpos($vars['ccaps'], 'complete') !== false) {
			$purchase_item = 'Tao of WordPress [PDF + Updates]';
		if (strpos($vars['item_name'], 'COUPON') !== false) {
			$coupon_code = $vars['item_name'];
		} else {
			$coupon_code = 'N/A';
		$message .= '<table>';
		$message .= '<tr><td><strong>Item Name:</strong> </td><td>' . strip_tags($purchase_item) . '</td></tr>';
		$message .= '<tr><td><strong>Coupon Code:</strong> </td><td>' . strip_tags($coupon_code) . '</td></tr>';
		$message .= '</table>';
		echo $message;

I actually ended up using this as the starting point for the invoice system now in place at the newly redesigned Perishable Press Books.

Get all user field values

Quick snippet to display all user field values:

$user_id = get_current_user_ID();
$user_field_values = get_user_field('my_field_ID', $user_id);

if (is_array($user_field_values)) {
	foreach($user_field_values as $value) echo $value.'<br />';
add_user_meta($user_id, 's2x_subscr_id', '%%s2x_subscr_id%%', true);

See reference link in the footer for the original/source post.

Bonus: Stop s2member from phoning home

By default, s2member Pro “phones home” with various and relatively benign data from your site. Here is a quick snippet to disable all of these “behind-the-scenes” reports:

add_filter('c_ws_plugin__s2member_pro_stats_pinger_enable', '__return_false');

As with other snippets, that would be added via s2member must-use plugin located in /wp-content/mu-plugins/s2-hacks.php.

References and Resources for s2member

About the Author
Jeff Starr = Web Developer. Security Specialist. WordPress Buff.
WP Themes In Depth: Build and sell awesome WordPress themes.
Perishable Press is operated by Jeff Starr, a professional web developer and book author with two decades of experience. Here you will find posts about web development, WordPress, security, and more »
Digging Into WordPress: Take your WordPress skills to the next level.
All free plugins updated and ready for WordPress 6.6 dropping next week. Pro plugin updates in the works :)
99% of video thumbnail/previews are pure cringe. Goofy faces = Clickbait.
Crazy that we’re almost halfway thru 2024.
I live right next door to the absolute loudest car in town. And the owner loves to drive it.
8G Firewall now out of beta testing, ready for use on production sites.
It's all about that ad revenue baby.
Get news, updates, deals & tips via email.
Email kept private. Easy unsubscribe anytime.