PluginProbe
s2Member – Excellent for All Kinds of Memberships, Content Restriction Paywalls & Member Access Subscriptions / 260917
s2Member – Excellent for All Kinds of Memberships, Content Restriction Paywalls & Member Access Subscriptions v260917
260917 260913 260909 260829 260814 260805 110710 110731 110812 110815 110912 110913 110915 110926 110927 111002 111003 111011 111017 111029 111105 111206 111216 111220 120213 All 189 releases
s2member / src / includes / classes / users-list.inc.php

users-list.inc.php in s2Member – Excellent for All Kinds of Memberships, Content Restriction Paywalls & Member Access Subscriptions 260917, at src/includes/classes/users-list.inc.php

626 lines 33.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 // @codingStandardsIgnoreFile
3 /**
4 * Users list.
5 *
6 * Copyright: © 2009-2011
7 * {@link http://websharks-inc.com/ WebSharks, Inc.}
8 * (coded in the USA)
9 *
10 * Released under the terms of the GNU General Public License.
11 * You should have received a copy of the GNU General Public License,
12 * along with this software. In the main directory, see: /licensing/
13 * If not, see: {@link http://www.gnu.org/licenses/}.
14 *
15 * @package s2Member\Users_List
16 * @since 3.5
17 */
18 if(!defined('WPINC')) // MUST have WordPress.
19 exit ("Do not access this file directly.");
20
21 if(!class_exists("c_ws_plugin__s2member_users_list"))
22 {
23 /**
24 * Users list.
25 *
26 * @package s2Member\Users_List
27 * @since 3.5
28 */
29 class c_ws_plugin__s2member_users_list
30 {
31 /**
32 * Adds Custom Fields to the admin Profile editing page.
33 *
34 * @package s2Member\Users_List
35 * @since 3.5
36 *
37 * @attaches-to ``add_action("edit_user_profile");``
38 * @attaches-to ``add_action("show_user_profile");``
39 *
40 * @param WP_User $user Expects a `WP_User` object passed in by the Action Hook.
41 */
42 public static function users_list_edit_cols($user = NULL)
43 {
44 c_ws_plugin__s2member_users_list_in::users_list_edit_cols($user);
45 }
46
47 /**
48 * Saves Custom Fields after an admin updates Profile.
49 *
50 * @package s2Member\Users_List
51 * @since 3.5
52 *
53 * @attaches-to ``add_action("edit_user_profile_update");``
54 * @attaches-to ``add_action("personal_options_update");``
55 *
56 * @param int|string $user_id Expects a numeric WordPress User ID passed in by the Action Hook.
57 */
58 public static function users_list_update_cols($user_id = NULL)
59 {
60 return c_ws_plugin__s2member_users_list_in::users_list_update_cols($user_id);
61 }
62
63 /**
64 * Modifies the search query.
65 *
66 * Affects searches performed in the list of Users.
67 *
68 * @package s2Member\Users_List
69 * @since 3.5
70 *
71 * @attaches-to ``add_action("pre_user_query");``
72 *
73 * @param WP_User_Query $query Expects a `WP_User_Query` object, by reference.
74 */
75 public static function users_list_query(&$query = NULL)
76 {
77 global $wpdb;
78 /** @var $wpdb wpdb */
79
80 foreach(array_keys(get_defined_vars()) as $__v) $__refs[$__v] =& $$__v;
81 do_action("ws_plugin__s2member_before_users_list_search", get_defined_vars());
82 unset($__refs, $__v);
83
84 if(is_admin() && !empty($GLOBALS['pagenow']) && $GLOBALS['pagenow'] === 'users.php')
85 if(isset ($query->query_vars) && !is_network_admin()) // NOT in Network admin panels.
86 if(is_array($qv = $query->query_vars) && ($s = trim($qv["search"], "* \t\n\r\0\x0B")) && ($s = "%".esc_sql(c_ws_plugin__s2member_utils_strings::like_escape($s))."%"))
87 {
88 //260915.0638 Searching Users used to JOIN every candidate user to all of their usermeta rows, then use DISTINCT to collapse the duplicate users. Membership sites can have many meta rows per user, so that greatly enlarges the intermediate result before MySQL can return a single page of Users. The normal path below keeps the users table as the main result and uses correlated EXISTS checks only for the specific meta keys s2Member searches. This produced the same IDs/counts while substantially reducing uncached search time on a large-user fixture.
89 $_search_sql_customized = has_filter("ws_plugin__s2member_before_users_list_search_where_or_before") !== false
90 || has_filter("ws_plugin__s2member_before_users_list_search_where_or_after") !== false
91 || has_filter("ws_plugin__s2member_before_users_list_search_from") !== false
92 || has_filter("ws_plugin__s2member_before_users_list_search_where") !== false;
93
94 if(!$_search_sql_customized)
95 {
96 //260915.0757 Keep usermeta out of the main FROM clause so one user remains one result row. EXISTS can stop looking once a relevant matching meta row is found, avoiding the legacy JOIN's row multiplication. Retain DISTINCT on the user ID for pagination/database-adapter compatibility; unlike the legacy query, it no longer has a large joined result to deduplicate.
97 $_meta = "___s2_search_meta";
98 $query->query_fields = "SQL_CALC_FOUND_ROWS DISTINCT(`".$wpdb->users."`.`ID`)";
99 $query->query_from = " FROM `".$wpdb->users."`";
100 //260915.0638 Preserve the legacy JOIN's subtle behavior: a users-table row with no usermeta at all was never returned, even when a normal user field matched.
101 $query->query_where = " WHERE EXISTS (SELECT 1 FROM `".$wpdb->usermeta."` `___s2_any_meta` WHERE `___s2_any_meta`.`user_id` = `".$wpdb->users."`.`ID`)";
102 $query->query_where .= " AND ((`".$wpdb->users."`.`user_login` LIKE '".$s."' OR `".$wpdb->users."`.`user_nicename` LIKE '".$s."' OR `".$wpdb->users."`.`display_name` LIKE '".$s."' OR `".$wpdb->users."`.`user_email` LIKE '".$s."' OR `".$wpdb->users."`.`user_url` LIKE '".$s."')";
103 $query->query_where .= " OR EXISTS (SELECT 1 FROM `".$wpdb->usermeta."` `".$_meta."` WHERE `".$_meta."`.`user_id` = `".$wpdb->users."`.`ID` AND (";
104 $query->query_where .= "((`".$_meta."`.`meta_key` = 'first_name' OR `".$_meta."`.`meta_key` = 'last_name') AND `".$_meta."`.`meta_value` LIKE '".$s."')";
105 $query->query_where .= " OR (`".$_meta."`.`meta_key` = '".$wpdb->base_prefix."s2member_subscr_id' AND `".$_meta."`.`meta_value` LIKE '".$s."')";
106 $query->query_where .= " OR (`".$_meta."`.`meta_key` = '".$wpdb->base_prefix."s2member_custom' AND `".$_meta."`.`meta_value` LIKE '".$s."')";
107 $query->query_where .= " OR (`".$_meta."`.`meta_key` = '".$wpdb->base_prefix."s2member_custom_fields' AND `".$_meta."`.`meta_value` LIKE '".$s."')";
108 if(apply_filters("ws_plugin__s2member_users_list_search_admin_notes", FALSE, get_defined_vars())) // Off by default; this can get very slow on large sites.
109 $query->query_where .= " OR (`".$_meta."`.`meta_key` = '".$wpdb->base_prefix."s2member_notes' AND `".$_meta."`.`meta_value` LIKE '".$s."')";
110 $query->query_where .= ")))";
111
112 if(is_multisite()) // On a Multisite Network we need to make sure we're searching only users w/ capabilities on this blog.
113 $query->query_where .= " AND `".$wpdb->users."`.`ID` IN(SELECT DISTINCT(`user_id`) FROM `".$wpdb->usermeta."` WHERE `meta_key` = '".$wpdb->prefix."capabilities')";
114 }
115 else
116 {
117 //260915.0638 Preserve the original JOIN-based query when an extension customizes s2Member's Users-search SQL. Those filters historically receive and may modify the legacy FROM/WHERE structure, so forcing the optimized query here could break integrations that depend on it.
118 $query->query_fields = "SQL_CALC_FOUND_ROWS DISTINCT(`".$wpdb->users."`.`ID`)";
119 $query->query_from = " FROM `".$wpdb->users."`, `".$wpdb->usermeta."`"; // Include meta table also.
120 $query->query_where = " WHERE `".$wpdb->users."`.`ID` = `".$wpdb->usermeta."`.`user_id`"; // Join w/ meta table.
121 $query->query_where .= " AND (".apply_filters("ws_plugin__s2member_before_users_list_search_where_or_before", "", get_defined_vars());
122 $query->query_where .= " (`".$wpdb->users."`.`user_login` LIKE '".$s."' OR `".$wpdb->users."`.`user_nicename` LIKE '".$s."' OR `".$wpdb->users."`.`display_name` LIKE '".$s."' OR `".$wpdb->users."`.`user_email` LIKE '".$s."' OR `".$wpdb->users."`.`user_url` LIKE '".$s."')";
123 $query->query_where .= " OR ((`".$wpdb->usermeta."`.`meta_key` = 'first_name' OR `".$wpdb->usermeta."`.`meta_key` = 'last_name') AND `".$wpdb->usermeta."`.`meta_value` LIKE '".$s."')";
124 $query->query_where .= " OR (`".$wpdb->usermeta."`.`meta_key` = '".$wpdb->base_prefix."s2member_subscr_id' AND `".$wpdb->usermeta."`.`meta_value` LIKE '".$s."')";
125 $query->query_where .= " OR (`".$wpdb->usermeta."`.`meta_key` = '".$wpdb->base_prefix."s2member_custom' AND `".$wpdb->usermeta."`.`meta_value` LIKE '".$s."')";
126 $query->query_where .= " OR (`".$wpdb->usermeta."`.`meta_key` = '".$wpdb->base_prefix."s2member_custom_fields' AND `".$wpdb->usermeta."`.`meta_value` LIKE '".$s."')";
127 if(apply_filters("ws_plugin__s2member_users_list_search_admin_notes", FALSE, get_defined_vars())) // Off by default; this can get very slow on large sites.
128 $query->query_where .= " OR (`".$wpdb->usermeta."`.`meta_key` = '".$wpdb->base_prefix."s2member_notes' AND `".$wpdb->usermeta."`.`meta_value` LIKE '".$s."')";
129 $query->query_where .= apply_filters("ws_plugin__s2member_before_users_list_search_where_or_after", "", get_defined_vars()).")"; // Leaving room for additional searches here.
130
131 if(is_multisite()) // On a Multisite Network we need to make sure we're searching only users w/ capabilities on this blog.
132 $query->query_where .= " AND `".$wpdb->users."`.`ID` IN(SELECT DISTINCT(`user_id`) FROM `".$wpdb->usermeta."` WHERE `meta_key` = '".$wpdb->prefix."capabilities')";
133
134 $query->query_from = apply_filters("ws_plugin__s2member_before_users_list_search_from", $query->query_from, get_defined_vars());
135 $query->query_where = apply_filters("ws_plugin__s2member_before_users_list_search_where", $query->query_where, get_defined_vars());
136 }
137 unset($_meta, $_search_sql_customized);
138 }
139
140 if(is_admin() && !is_network_admin() && !empty($GLOBALS['pagenow']) && $GLOBALS['pagenow'] === 'users.php' && !empty($_GET['s2member_view']) && isset($query->query_vars))
141 {
142 $is_eot_current_view = $_GET['s2member_view'] === 'eot_current';
143 $is_eot_previous_view = $_GET['s2member_view'] === 'eot_previous';
144
145 if($is_eot_current_view || $is_eot_previous_view)
146 {
147 $current_eot_key = esc_sql($wpdb->prefix.'s2member_auto_eot_time');
148 $last_eot_key = esc_sql($wpdb->prefix.'s2member_last_auto_eot_time');
149 $processed_eot_key = esc_sql($wpdb->prefix.'s2member_last_auto_eot_processed_time');
150
151 if($is_eot_current_view)
152 {
153 //260828.0231 Current End-of-Term is the actionable queue: only users with a current EOT belong here, so native ascending EOT sorting never has blank rows to place first.
154 $query->query_where .= " AND EXISTS (SELECT 1 FROM `".$wpdb->usermeta."` `___s2_eot_current` WHERE `___s2_eot_current`.`user_id` = `".$wpdb->users."`.`ID` AND `___s2_eot_current`.`meta_key` = '".$current_eot_key."' AND CAST(`___s2_eot_current`.`meta_value` AS UNSIGNED) > 0)";
155
156 if(empty($_REQUEST['orderby']) && empty($_REQUEST['s']))
157 {
158 $query->query_from .= " LEFT JOIN `".$wpdb->usermeta."` `___s2_eot_order_current` ON (`".$wpdb->users."`.`ID` = `___s2_eot_order_current`.`user_id` AND `___s2_eot_order_current`.`meta_key` = '".$current_eot_key."')";
159 $query->query_orderby = "ORDER BY CAST(`___s2_eot_order_current`.`meta_value` AS UNSIGNED) ASC";
160 }
161 }
162 else
163 {
164 //260828.0231 Previous End-of-Term includes either archived EOT or demotion history so partial/legacy records remain discoverable without mixing them into the current queue.
165 $query->query_where .= " AND (EXISTS (SELECT 1 FROM `".$wpdb->usermeta."` `___s2_eot_last` WHERE `___s2_eot_last`.`user_id` = `".$wpdb->users."`.`ID` AND `___s2_eot_last`.`meta_key` = '".$last_eot_key."' AND CAST(`___s2_eot_last`.`meta_value` AS UNSIGNED) > 0) OR EXISTS (SELECT 1 FROM `".$wpdb->usermeta."` `___s2_eot_processed` WHERE `___s2_eot_processed`.`user_id` = `".$wpdb->users."`.`ID` AND `___s2_eot_processed`.`meta_key` = '".$processed_eot_key."' AND CAST(`___s2_eot_processed`.`meta_value` AS UNSIGNED) > 0))";
166
167 if(empty($_REQUEST['orderby']) && empty($_REQUEST['s']))
168 {
169 //260828.0358 Previous End-of-Term is a processing-history list, so show the most recently processed EOT actions first; records without a recoverable demotion time naturally remain after dated records.
170 $query->query_from .= " LEFT JOIN `".$wpdb->usermeta."` `___s2_eot_order_processed` ON (`".$wpdb->users."`.`ID` = `___s2_eot_order_processed`.`user_id` AND `___s2_eot_order_processed`.`meta_key` = '".$processed_eot_key."')";
171 $query->query_orderby = "ORDER BY CAST(`___s2_eot_order_processed`.`meta_value` AS UNSIGNED) DESC";
172 }
173 }
174 }
175 }
176
177 if(is_admin() && !is_network_admin() && !empty($GLOBALS['pagenow']) && $GLOBALS['pagenow'] === 'users.php' && !empty($_GET['role']) && $_GET['role'] === 's2member_pending_deletion' && isset($query->query_vars) && empty($_REQUEST['orderby']) && empty($_REQUEST['s']))
178 {
179 //260828.0317 Pending Deletion is an action queue, so show the accounts waiting longest first by their actual EOT demotion time instead of WordPress's Username default.
180 $processed_eot_key = esc_sql($wpdb->prefix.'s2member_last_auto_eot_processed_time');
181 $query->query_from .= " LEFT JOIN `".$wpdb->usermeta."` `___s2_pending_order_processed` ON (`".$wpdb->users."`.`ID` = `___s2_pending_order_processed`.`user_id` AND `___s2_pending_order_processed`.`meta_key` = '".$processed_eot_key."')";
182 $query->query_orderby = "ORDER BY CAST(`___s2_pending_order_processed`.`meta_value` AS UNSIGNED) ASC";
183 }
184 foreach(array_keys(get_defined_vars()) as $__v) $__refs[$__v] =& $$__v;
185 do_action("ws_plugin__s2member_after_users_list_search", get_defined_vars());
186 unset($__refs, $__v);
187 }
188
189 /**
190 * Adds columns to the list of Users.
191 *
192 * @package s2Member\Users_List
193 * @since 3.5
194 *
195 * @attaches-to ``add_filter ("manage_users_columns");``
196 *
197 * @param array $cols Expects an array of columns to be passed through by the Filter.
198 *
199 * @return array Array of columns, merged with columns introduced by this routine.
200 */
201 public static function users_list_cols($cols = array())
202 {
203 foreach(array_keys(get_defined_vars()) as $__v) $__refs[$__v] =& $$__v;
204 do_action("ws_plugin__s2member_before_users_list_cols", get_defined_vars());
205 unset($__refs, $__v);
206
207 $cols["s2member_registration_time"] = "Registration Date";
208
209 if(apply_filters("ws_plugin__s2member_users_list_cols_display_paid_registration_times", FALSE))
210 $cols["s2member_paid_registration_times"] = "Paid Registr. Date";
211
212 $cols["s2member_subscr_id"] = "Paid Subscr. ID";
213
214 if(!is_multisite() || !c_ws_plugin__s2member_utils_conds::is_multisite_farm() || is_main_site())
215 $cols["s2member_ccaps"] = "Custom Capabilities";
216
217 $cols["s2member_auto_eot_time"] = "EOT Time";
218 //260822.1509 Keep the triggering EOT and its actual processing time separate; both remain optional history columns on the normal Users screen.
219 $cols["s2member_last_auto_eot_time"] = "Last EOT";
220 $cols["s2member_last_auto_eot_processed_time"] = "EOT Demotion";
221
222 if($GLOBALS["WS_PLUGIN__"]["s2member"]["o"]["custom_reg_fields"])
223 foreach(json_decode($GLOBALS["WS_PLUGIN__"]["s2member"]["o"]["custom_reg_fields"], TRUE) as $field)
224 {
225 $field_var = preg_replace("/[^a-z0-9]/i", "_", strtolower($field["id"]));
226 $field_id_class = preg_replace("/_/", "-", $field_var);
227
228 $field_title = ucwords(preg_replace("/_/", " ", $field_var));
229 $cols["s2member_custom_field_".$field_var] = $field_title;
230 }
231
232 $cols["s2member_login_counter"] = "# Of Logins";
233 $cols["s2member_last_login_time"] = "Last Login Time";
234
235 foreach(array_keys(get_defined_vars()) as $__v) $__refs[$__v] =& $$__v;
236 do_action("ws_plugin__s2member_during_users_list_cols", get_defined_vars());
237 unset($__refs, $__v);
238
239 return apply_filters("ws_plugin__s2member_users_list_cols", $cols, get_defined_vars());
240 }
241
242 /**
243 * Re-adds required EOT columns late on s2Member's review views after column-management plugins have filtered the Users table.
244 *
245 * @package s2Member\Users_List
246 * @since 260826.0542
247 *
248 * @attaches-to ``add_filter("manage_users_columns");``
249 *
250 * @param array $cols User list columns after other plugins have filtered them.
251 *
252 * @return array User list columns with the required EOT review columns restored.
253 */
254 public static function users_list_required_eot_cols($cols = array())
255 {
256 if(!is_admin() || empty($GLOBALS['pagenow']) || $GLOBALS['pagenow'] !== 'users.php')
257 return $cols;
258
259 $is_eot_current_view = !empty($_GET['s2member_view']) && $_GET['s2member_view'] === 'eot_current';
260 $is_eot_previous_view = !empty($_GET['s2member_view']) && $_GET['s2member_view'] === 'eot_previous';
261 $is_pending_deletion_view = !empty($_GET['role']) && $_GET['role'] === 's2member_pending_deletion';
262
263 //260828.0231 Restore only the columns required by each s2Member review view after Users-table customization plugins apply saved layouts.
264 if($is_eot_current_view || $is_pending_deletion_view)
265 $cols['s2member_auto_eot_time'] = 'EOT Time';
266
267 if($is_eot_previous_view || $is_pending_deletion_view)
268 {
269 $cols['s2member_last_auto_eot_time'] = 'Last EOT';
270 $cols['s2member_last_auto_eot_processed_time'] = 'EOT Demotion';
271 }
272 return $cols;
273 }
274
275 /**
276 * Hides optional s2Member history columns by default on the Users screen.
277 *
278 * @package s2Member\Users_List
279 * @since 260822.1509
280 *
281 * @attaches-to ``add_filter("default_hidden_columns");``
282 *
283 * @param array $hidden Default hidden column IDs.
284 * @param WP_Screen $screen Current screen object.
285 *
286 * @return array Filtered hidden column IDs.
287 */
288 public static function users_list_default_hidden_cols($hidden = array(), $screen = NULL)
289 {
290 if(is_object($screen) && !empty($screen->id) && $screen->id === 'users')
291 {
292 //260822.1509 Preserve the normal Users table's compact default while leaving both EOT history columns available through Screen Options.
293 $hidden[] = 's2member_last_auto_eot_time';
294 $hidden[] = 's2member_last_auto_eot_processed_time';
295 $hidden = array_values(array_unique($hidden));
296 }
297 return $hidden;
298 }
299
300 /**
301 * 260828.0231 Exposes the required End-of-Term columns in s2Member's Users review views.
302 *
303 * @package s2Member\Users_List
304 * @since 260822.1519
305 *
306 * @attaches-to ``add_filter("hidden_columns");``
307 *
308 * @param array $hidden Hidden column IDs.
309 * @param WP_Screen $screen Current screen object.
310 *
311 * @return array Filtered hidden column IDs.
312 */
313 public static function users_list_hidden_cols($hidden = array(), $screen = NULL)
314 {
315 if(is_object($screen) && !empty($screen->id) && $screen->id === 'users')
316 {
317 $is_eot_current_view = !empty($_GET['s2member_view']) && $_GET['s2member_view'] === 'eot_current';
318 $is_eot_previous_view = !empty($_GET['s2member_view']) && $_GET['s2member_view'] === 'eot_previous';
319 $is_pending_deletion_view = !empty($_GET['role']) && $_GET['role'] === 's2member_pending_deletion';
320
321 //260828.0231 Each review view forces only its primary timestamps visible; other Users columns continue to follow the administrator's Screen Options.
322 if($is_eot_current_view)
323 $hidden = array_diff($hidden, array('s2member_auto_eot_time'));
324 else if($is_eot_previous_view)
325 $hidden = array_diff($hidden, array('s2member_last_auto_eot_time', 's2member_last_auto_eot_processed_time'));
326 else if($is_pending_deletion_view)
327 $hidden = array_diff($hidden, array('s2member_auto_eot_time', 's2member_last_auto_eot_time', 's2member_last_auto_eot_processed_time'));
328
329 $hidden = array_values($hidden);
330 }
331 return $hidden;
332 }
333
334 /**
335 * 260828.0231 Adds the Current and Previous End-of-Term reports to WordPress' native Users views.
336 *
337 * @package s2Member\Users_List
338 * @since 260822.1519
339 *
340 * @attaches-to ``add_filter("views_users");``
341 *
342 * @param array $views Native Users view links.
343 *
344 * @return array Filtered Users view links.
345 */
346 public static function users_list_views($views = array())
347 {
348 global $wpdb;
349
350 if(!is_admin() || is_network_admin() || empty($GLOBALS['pagenow']) || $GLOBALS['pagenow'] !== 'users.php')
351 return $views;
352
353 $is_eot_current_view = !empty($_GET['s2member_view']) && $_GET['s2member_view'] === 'eot_current';
354 $is_eot_previous_view = !empty($_GET['s2member_view']) && $_GET['s2member_view'] === 'eot_previous';
355 $is_pending_deletion_view = !empty($_GET['role']) && $_GET['role'] === 's2member_pending_deletion';
356 $current_eot_key = $wpdb->prefix.'s2member_auto_eot_time';
357 $last_eot_key = $wpdb->prefix.'s2member_last_auto_eot_time';
358 $processed_eot_key = $wpdb->prefix.'s2member_last_auto_eot_processed_time';
359 $has_current_eot_users = $is_eot_current_view || (bool)$wpdb->get_var($wpdb->prepare("SELECT 1 FROM `".$wpdb->usermeta."` WHERE `meta_key` = %s AND CAST(`meta_value` AS UNSIGNED) > 0 LIMIT 1", $current_eot_key));
360 $has_previous_eot_users = $is_eot_previous_view || (bool)$wpdb->get_var($wpdb->prepare("SELECT 1 FROM `".$wpdb->usermeta."` WHERE `meta_key` IN (%s, %s) AND CAST(`meta_value` AS UNSIGNED) > 0 LIMIT 1", $last_eot_key, $processed_eot_key));
361
362 if(($is_eot_current_view || $is_eot_previous_view) && isset($views['all']))
363 $views['all'] = str_replace(' class="current" aria-current="page"', '', $views['all']);
364
365 //260828.0231 Keep each EOT report out of unused Users screens, while direct report URLs remain self-identifying even when the selected report is empty.
366 if($has_current_eot_users)
367 {
368 $url = add_query_arg('s2member_view', 'eot_current', admin_url('users.php'));
369 $views['s2member_eot_current'] = '<a href="'.esc_url($url).'"'.($is_eot_current_view ? ' class="current" aria-current="page"' : '').'>End-of-Term Current</a>';
370 }
371
372 if($has_previous_eot_users)
373 {
374 $url = add_query_arg('s2member_view', 'eot_previous', admin_url('users.php'));
375 $views['s2member_eot_previous'] = '<a href="'.esc_url($url).'"'.($is_eot_previous_view ? ' class="current" aria-current="page"' : '').'>End-of-Term Previous</a>';
376 }
377
378 $pending_deletion_view = isset($views['s2member_pending_deletion']) ? $views['s2member_pending_deletion'] : '';
379 unset($views['s2member_pending_deletion']);
380 $delete_behavior = isset($GLOBALS['WS_PLUGIN__']['s2member']['o']['membership_eot_behavior']) && $GLOBALS['WS_PLUGIN__']['s2member']['o']['membership_eot_behavior'] === 'delete';
381 //260828.0337 WordPress's native role view already proves pending users exist; preserve it before any fallback query can inherit an active s2Member EOT report filter.
382 $has_pending_deletion_users = $is_pending_deletion_view || (bool)$pending_deletion_view || (!$delete_behavior && (bool)get_users(array('role' => 's2member_pending_deletion', 'number' => 1, 'fields' => 'ID')));
383
384 //260823.0130 Pending Deletion is useful navigation while "Delete" is configured or review work exists; hide the otherwise-empty role link without unregistering the role or affecting direct access.
385 if($delete_behavior || $has_pending_deletion_users)
386 {
387 if(!$pending_deletion_view)
388 {
389 $url = add_query_arg('role', 's2member_pending_deletion', admin_url('users.php'));
390 $pending_deletion_view = '<a href="'.esc_url($url).'"'.($is_pending_deletion_view ? ' class="current" aria-current="page"' : '').'>Pending Deletion <span class="count">(0)</span></a>';
391 }
392 $views['s2member_pending_deletion'] = $pending_deletion_view;
393 }
394
395 return $views;
396 }
397
398 /**
399 * 260828.0231 Explains the native Current and Previous End-of-Term Users views.
400 *
401 * @package s2Member\Users_List
402 * @since 260823.0302
403 *
404 * @attaches-to ``add_action("admin_notices");``
405 *
406 * @return null
407 */
408 public static function users_list_eot_notice()
409 {
410 if(!is_admin() || is_network_admin() || empty($GLOBALS['pagenow']) || $GLOBALS['pagenow'] !== 'users.php' || empty($_GET['s2member_view']))
411 return;
412
413 if($_GET['s2member_view'] === 'eot_current')
414 {
415 //260828.0358 Describe this as a user-facing list rather than WordPress's internal "view" terminology; oldest/overdue current EOTs remain first.
416 c_ws_plugin__s2member_admin_notices::display_admin_notice('This list includes users with a current End-of-Term time, ordered by the earliest EOT Time first.');
417 }
418 else if($_GET['s2member_view'] === 'eot_previous')
419 {
420 //260828.0358 Previous EOTs are processing-history records, so explain both the historical scope and the EOT Demotion ordering directly.
421 c_ws_plugin__s2member_admin_notices::display_admin_notice('This list includes users who had an End-of-Term time before, ordered by the most recent EOT Demotion first.');
422 }
423 }
424
425 /**
426 * Explains the native Pending Deletion role view.
427 *
428 * @package s2Member\Users_List
429 * @since 260822.1519
430 *
431 * @attaches-to ``add_action("admin_notices");``
432 *
433 * @return null
434 */
435 public static function users_list_pending_deletion_notice()
436 {
437 if(!is_admin() || is_network_admin() || empty($GLOBALS['pagenow']) || $GLOBALS['pagenow'] !== 'users.php' || empty($_GET['role']) || $_GET['role'] !== 's2member_pending_deletion')
438 return;
439
440 //260828.0358 Describe Pending Deletion as a list rather than a WordPress "view," while explaining why these accounts remain available for administrator review.
441 c_ws_plugin__s2member_admin_notices::display_admin_notice('This list includes accounts preserved in <strong>Pending Deletion</strong> after their membership access was removed at End-of-Term. Use WordPress\'s normal bulk Delete action when you are ready to delete them.');
442 }
443
444 /**
445 * Displays column data in the row of details.
446 *
447 * @package s2Member\Users_List
448 * @since 3.5
449 *
450 * @attaches-to ``add_filter ("manage_users_custom_column");``
451 *
452 * @param string $val A value for this column, passed through by the Filter.
453 * @param string $col The name of the column for which we might need to supply data for.
454 * @param int|string $user_id Expects a WordPress User ID, passed through by the Filter.
455 *
456 * @return string A column value introduced by this routine, or existing value, or, if empty, a dash.
457 */
458 public static function users_list_display_cols($val = '', $col = '', $user_id = '')
459 {
460 static $user, $last_user_id; // Used internally for optimization.
461 static $fields, $last_fields_id; // Used for optimization.
462
463 foreach(array_keys(get_defined_vars()) as $__v) $__refs[$__v] =& $$__v;
464 do_action("ws_plugin__s2member_before_users_list_display_cols", get_defined_vars());
465 unset($__refs, $__v);
466
467 $user = (is_object($user) && $user_id === $last_user_id) ? $user : new WP_User ($user_id);
468
469 if($col === "s2member_registration_time")
470 $val = (($time = strtotime(get_date_from_gmt($user->user_registered)))) ? esc_html(date("D M jS, Y", $time)).'<br /><small>@ precisely '.esc_html(date("g:i a", $time)).'</small>' : "";
471
472 else if($col === "s2member_paid_registration_times")
473 {
474 $val = ""; // Initialize $val before we begin.
475 if(is_array($v = get_user_option("s2member_paid_registration_times", $user_id)))
476 foreach($v as $level => $time) // Go through each Paid Registration Time.
477 {
478 $time = strtotime(get_date_from_gmt(date("Y-m-d H:i:s", $time)));
479
480 if($level === "level") // First Payment Time, regardless of Level.
481 $val .= (($val) ? "<br />" : "").'<span title="'.esc_attr(date("D M jS, Y", $time)).' @ precisely '.esc_attr(date("g:i a", $time)).'">'.esc_html(date("D M jS, Y", $time)).'</span>';
482 else if(preg_match("/^level([0-9]+)$/i", $level) && ($level = preg_replace("/^level/", "", $level)))
483 $val .= (($val) ? "<br />" : "").'<small><em>@Level '.esc_html($level).': <span title="'.esc_attr(date("D M jS, Y", $time)).' @ precisely '.esc_attr(date("g:i a", $time)).'">'.esc_html(date("D M jS, Y", $time)).'</span></em></small>';
484 }
485 }
486 else if($col === "s2member_subscr_id")
487 $val = ($v = get_user_option("s2member_subscr_id", $user_id)) ? esc_html($v) : "";
488
489 else if($col === "s2member_ccaps") // Custom Capabilities.
490 {
491 foreach($user->allcaps as $cap => $cap_enabled)
492 if(preg_match("/^access_s2member_ccap_/", $cap))
493 $ccaps[] = preg_replace("/^access_s2member_ccap_/", "", $cap);
494
495 $val = (!empty($ccaps)) ? implode("<br />", $ccaps) : "";
496 }
497 else if($col === "s2member_auto_eot_time")
498 $val = ($v = get_user_option("s2member_auto_eot_time", $user_id)) ? date("D M jS, Y", (int)$v)."<br /><small>@ precisely ".date("g:i a", (int)$v)."</small>" : "";
499
500 else if($col === "s2member_last_auto_eot_time" || $col === "s2member_last_auto_eot_processed_time")
501 {
502 //260822.1509 Match the existing EOT Time presentation so current, triggering, and processed timestamps compare directly in the same Users table.
503 $val = ($v = get_user_option($col, $user_id)) ? date("D M jS, Y", (int)$v)."<br /><small>@ precisely ".date("g:i a", (int)$v)."</small>" : "";
504 }
505
506 else if(preg_match("/^s2member_custom_field_/", $col))
507 {
508 if(!$last_fields_id || $last_fields_id !== $user_id)
509 $fields = get_user_option("s2member_custom_fields", $user_id);
510
511 $field_var = preg_replace("/^s2member_custom_field_/", "", $col);
512
513 if(isset ($fields[$field_var]) && is_string($fields[$field_var]) && preg_match("/^http(s?)\:/i", $fields[$field_var]))
514 $val = '<a href="'.esc_attr($fields[$field_var]).'" target="_blank">'.esc_html(substr($fields[$field_var], strpos($fields[$field_var], ":") + 3, 25)."...").'</a>';
515
516 else if(isset ($fields[$field_var]) && is_array($fields[$field_var]) && !empty($fields[$field_var]))
517 $val = preg_replace("/-\|br\|-/", "<br />", esc_html(implode("-|br|-", $fields[$field_var])));
518
519 else if(isset ($fields[$field_var]) && is_string($fields[$field_var]) && strlen($fields[$field_var]))
520 $val = esc_html($fields[$field_var]);
521
522 $last_fields_id = $user_id; // Record this.
523 }
524 else if($col === "s2member_login_counter")
525 $val = ($v = get_user_option("s2member_login_counter", $user_id)) ? esc_html($v) : "";
526
527 else if($col === "s2member_last_login_time")
528 {
529 if(($time = get_user_option("s2member_last_login_time", $user_id)))
530 {
531 $time = strtotime(get_date_from_gmt(date("Y-m-d H:i:s", $time)));
532 $val = esc_html(date("D M jS, Y", $time)).'<br /><small>@ precisely '.esc_html(date("g:i a", $time)).'</small>';
533 }
534 else $val = ""; // Not applicable (we've never recorded them logging into the site).
535 }
536 $last_user_id = $user_id; // Record this for internal optimizations.
537
538 return apply_filters("ws_plugin__s2member_users_list_display_cols", ((strlen((string) $val)) ? $val : ""), get_defined_vars());
539 }
540
541 /**
542 * Tells WordPress certain fields s2Member adds are sortable
543 *
544 * @package s2Member\Users_List
545 * @since 140518
546 *
547 * @attaches-to ``add_filter ("manage_users_sortable_columns");``
548 *
549 * @param array $columns An array of sortable user list columns.
550 *
551 * @return array The input array after having been filtered here.
552 */
553 public static function users_list_add_sortable($columns = array())
554 {
555 if(!empty($_REQUEST['s']))
556 return $columns;
557
558 $is_eot_current_view = !empty($_GET['s2member_view']) && $_GET['s2member_view'] === 'eot_current';
559 $is_eot_previous_view = !empty($_GET['s2member_view']) && $_GET['s2member_view'] === 'eot_previous';
560 $is_pending_deletion_view = !empty($_GET['role']) && $_GET['role'] === 's2member_pending_deletion';
561 if($is_eot_current_view || $is_eot_previous_view || $is_pending_deletion_view)
562 {
563 //260828.0317 WordPress normally marks Username as initially sorted; each EOT review view instead exposes its own default timestamp and direction in the native header state.
564 foreach($columns as $_column_key => $_column_data)
565 if(is_array($_column_data) && isset($_column_data[4]))
566 $columns[$_column_key][4] = false;
567 }
568
569 $columns['s2member_registration_time'] = 's2member_registration_time';
570 $columns['s2member_subscr_id'] = 's2member_subscr_id';
571 $columns['s2member_auto_eot_time'] = $is_eot_current_view ? array('s2member_auto_eot_time', false, 'EOT Time', 'Table ordered by current End-of-Term time.', 'asc') : 's2member_auto_eot_time';
572 $columns['s2member_last_auto_eot_time'] = 's2member_last_auto_eot_time';
573 $columns['s2member_last_auto_eot_processed_time'] = $is_eot_previous_view ? array('s2member_last_auto_eot_processed_time', false, 'EOT Demotion', 'Table ordered by End-of-Term demotion time.', 'desc') : ($is_pending_deletion_view ? array('s2member_last_auto_eot_processed_time', false, 'EOT Demotion', 'Table ordered by End-of-Term demotion time.', 'asc') : 's2member_last_auto_eot_processed_time');
574 $columns['s2member_login_counter'] = 's2member_login_counter';
575 $columns['s2member_last_login_time'] = 's2member_last_login_time';
576
577 return $columns;
578 }
579
580 /**
581 * Alters WP_Query object to make custom columns sortable
582 *
583 * @package s2Member\Users_List
584 * @since 140518
585 *
586 * @attaches-to ``add_filter ("pre_user_query");``
587 *
588 * @param WP_User_Query $query `WP_Query` Object passed from WordPress
589 */
590 public static function users_list_make_sortable($query)
591 {
592 if(!is_admin()
593 || empty($GLOBALS['pagenow']) || $GLOBALS['pagenow'] !== 'users.php'
594 || !isset ($query->query_vars) || !empty($_REQUEST['s'])
595 ) return;
596
597 global $wpdb;
598 /** @var $wpdb wpdb */
599 $vars = $query->query_vars;
600
601 switch($vars['orderby'])
602 {
603 // This isn't a usermeta value, so we don't need to `LEFT JOIN` here.
604 case 's2member_registration_time':
605 $query->query_orderby = "ORDER BY CAST(`user_registered` AS UNSIGNED) ".$vars['order'];
606 break;
607
608 // s2Member Subscription ID can contain non-integer characters. We don't `CAST` this value as `UNSIGNED`
609 case 's2member_subscr_id':
610 $query->query_from .= " LEFT JOIN `".$wpdb->usermeta."` `___m` ON (`".$wpdb->users."`.`ID` = `___m`.`user_id` AND `___m`.`meta_key` = '".esc_sql($wpdb->prefix.$vars['orderby'])."')";
611 $query->query_orderby = "ORDER BY `___m`.`meta_value` ".$vars['order'];
612 break;
613
614 case 's2member_auto_eot_time':
615 case 's2member_last_auto_eot_time':
616 case 's2member_last_auto_eot_processed_time':
617 case 's2member_login_counter':
618 case 's2member_last_login_time':
619 $query->query_from .= " LEFT JOIN `".$wpdb->usermeta."` `___m` ON (`".$wpdb->users."`.`ID` = `___m`.`user_id` AND `___m`.`meta_key` = '".esc_sql($wpdb->prefix.$vars['orderby'])."')";
620 $query->query_orderby = "ORDER BY CAST(`___m`.`meta_value` AS UNSIGNED) ".$vars['order'];
621 break;
622 }
623 }
624 }
625 }
626