| 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 |
|