Auto EOT not working consistently

Yes, I installed it and checked. No, I don’t see any s2Member function there. There is one called wp_scheduled_delete(), but I think that’s part of wp, not s2Member.

I have set up a test with the server cron job running the following every hour (is that frequent enough?), and set s2Member to allow me to run auto EOT myself through cron jobs:
wget https://[domain name]/?s2member_auto_eot_system_via_cron=1 -O /dev/null

I created three test users with expirations one hour from now, tomorrow, and the day after tomorrow. We will see if they get expired properly.

Do you know how to create a piece of PHP code and SQL query that would search for all members who’s EOT has already passed and then change their role to Expired Member?

No, I don’t. It sounds like there is a bug in s2Member. Would you like to open an issue on Github or shall I?

A problem for us is that we’re running an outdated version of s2Member, and I don’t think we can update because of some customization that was done. Would it still be relevant to post to Github, considering this situation?

No, it wouldn’t, unless the customization is in an mu-plugin.

I will see if I can come up with some code to deal with this, but that might take a couple of weeks.

I have now opened a Github issue. I will see what I can do about writing some code for your situation but, as I said, it might take a while.

Thanks, Tim, much appreciated. I’ve got a start on the code. This finds the users who’s EOT has already passed:

SELECT * FROM [database].usermeta WHERE meta_key LIKE ‘%s2member_auto_eot_time%’ AND meta_value < now()

And the following should retrieve all users who aren’t ‘subscriber’. Subscriber is the role of expired members, as far as I can tell, so it should list all members who ARE NOT expired:

SELECT user_id FROM [database].usermeta WHERE meta_key LIKE ‘%capabilities%’ AND meta_value NOT LIKE ‘%subscriber%’

The part I’m stuck on right now is how to join these two based on the user_id field: I want to get a list of all user_ids that have a record matching the first query and the second query. But, because these are in the same table, I don’t know how to do the join.

Update: I may have figured this out. Here’s the query I’ve got currently, and it seems to work as

CREATE TEMPORARY TABLE IF NOT EXISTS usermeta_temp1 AS (SELECT * FROM [database].usermeta WHERE meta_key LIKE ‘%s2member_auto_eot_time%’ AND meta_value < now());
CREATE TEMPORARY TABLE IF NOT EXISTS usermeta_temp2 AS (SELECT user_id FROM [database].usermeta WHERE meta_key LIKE ‘%capabilities%’ AND meta_value NOT LIKE ‘%subscriber%’);
SELECT * FROM usermeta_temp1 INNER JOIN usermeta_temp2 ON usermeta_temp1.user_id = usermeta_temp2.user_id;

What do you think?

Nile, it isn’t the way I’d do it, because I’d prefer to use WP functions rather than interacting directly with the database, but that doesn’t mean your way is wrong. I was thinking more along these lines:

<?php

function kts_delete_members_at_eot() {
	$args = array(
		'role__in' => array( 's2member_level1', 's2member_level2', 's2member_level3', 's2member_level4' ),
	);
	$users = get_users( $args );

	foreach ( $users as $user ) {
		$eot = get_user_meta( $user->ID, 's2member_auto_eot_time', true );
		if ( current_time( 'mysql' ) > $eot ) {
			wp_delete_user( $user->ID );
		}
	}
}
add_action( 'init', 'kts_delete_members_at_eot' );

Note: that’s completely untested.

But if your method works, it works.

Thanks, Tim. I wold prefer to use Wordpress instead of interacting directly with the database, but wasn’t having much success figuring out how. Thank you for your example of concept.

Two questions about your method:

  1. Does it loop through every single user in the database? If yes, this seems really inefficient – it would be much faster if a search could be done to retrieve only users who’s EOT had already passed and who were not of role = subscriber.

  2. Does it delete the user who’s EOT has already passed? We need our code to change their role to ‘subscriber’, so the user is still in the database, but becomes an expired member.

Nile, on (1), the code is going to loop through every user to some extent, whether you do it by including or excluding roles (and whether via WP or directly within the database). The only question really is whether it’s faster to do it by including roles or by excluding them. But, when it comes to the deletion part, the list of users with the relevant roles is already stored as a variable, and so there’s no further call to the database for every user: only for those with the relevant roles.

If you think doing it by excluding roles would be better, you could use 'role__not_in' instead of 'role__in'. But that will then include administrators and the like, unless you add them into this array too.

On (2), yes, it deletes a member whose EOT has passed. If you want just to change the role, then you’d need something like $user->set_role( 'subscriber');

The real inefficiency of my code (if it works) is the fact that it will run on every page load because of the init hook. It should really be a cron function, but I don’t have time at the moment to work that bit out.

I have now tested some code. This seems to work:

<?php
function kts_delete_members_at_eot() {
	$args = array(
		'role__in' => array( 's2member_level1', 's2member_level2', 's2member_level3', 's2member_level4' ),
		'meta_key' => 'wp_s2member_auto_eot_time',
		'fields' => 'all_with_meta',
	);
	$users = get_users( $args );
	if ( empty( $users ) ) {
		return;
	}
	$time = time();

	foreach ( $users as $user ) {
		$eot = get_user_option( 'wp_s2member_auto_eot_time', $user->ID );
		if ( empty( $eot ) ) {
			return;
		}
		if ( $time > $eot ) {
			//wp_delete_user( $user->ID ); // deletes member
			$user->set_role( 'subscriber' ); // changes member's role
		}
	}
}
add_action( 'init', 'kts_delete_members_at_eot' );

You can add this as an mu-plugin. It will run on every page load. I will now experiment with getting it to run as a cronjob.

This should do it as a cronjob (now modified so as to work with any database prefix):

<?php
if ( !wp_next_scheduled( 'my_hourly_event' ) ) {
	wp_schedule_event( time(), 'hourly', 'my_hourly_event' );
}

function kts_delete_members_at_eot() {
	global $wpdb;
	$args = array(
		'role__in' => array( 's2member_level1', 's2member_level2', 's2member_level3', 's2member_level4' ),
		'meta_key' => $wpdb->prefix . 's2member_auto_eot_time',
		'fields' => 'all_with_meta',
	);
	$users = get_users( $args );
	if ( empty( $users ) ) {
		return;
	}
	$time = time();

	foreach ( $users as $user ) {
		$eot = get_user_option( $wpdb->prefix . 's2member_auto_eot_time', $user->ID );
		if ( empty( $eot ) ) {
			return;
		}
		if ( $time > $eot ) {
			//wp_delete_user( $user->ID );
			$user->set_role( 'subscriber' );
		}
	}
}
add_action( 'my_hourly_event', 'kts_delete_members_at_eot' );

@bask, It looks like the EOT cronjob sometimes just unsets itself. I don’t know why. But you might be able to get it working simply by resetting it again.

that’s not a new problem - I reported it a couple of times since over 4 years. It’s the only cronjob on my websites ever deleting itself - I guess there is some memory leak on that job and even if you setup php to jun jobs for a couple of hours and attribute a couple of GB to each job (my server to 128GB so I don’t care) it will disable itself at some point. S2member should either fix it or set up a second cron job that runs daily and checks if the cron job got deleted and then reset it…

Could I just run this by loading a PHP page through my web browser? I tried this, but it didn’t work:

<?php require_once("wp-load.php"); $args = array( 'role__in' => array( 's2member_level1', 's2member_level2', 's2member_level3', 's2member_level4' ), 'meta_key' => 'wp_s2member_auto_eot_time', 'fields' => 'all_with_meta', ); $users = get_users( $args ); if ( empty( $users ) ) { return; } $time = time(); foreach ( $users as $user ) { $eot = get_user_option( 'wp_s2member_auto_eot_time', $user->ID ); if ( empty( $eot ) ) { return; } if ( $time > $eot ) { //wp_delete_user( $user->ID ); // deletes member $user->set_role( 'subscriber' ); // changes member's role } } ?>

You could use my original code, and just add it to a different hook (e.g 'save_post') then perform the action associated with the hook (in this case, save a post) and then remove the code (or you could leave it there to be used in future).

Is possible to run it as a standalone PHP page that I can load through a web browser?

I tried the code this way, and I get no users found ($users is empty).