| @@ -84,27 +84,104 @@ | ||
| 84 | 84 | if(is_admin() && !empty($GLOBALS['pagenow']) && $GLOBALS['pagenow'] === 'users.php') |
| 85 | 85 | if(isset ($query->query_vars) && !is_network_admin()) // NOT in Network admin panels. |
| 86 | 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 | 87 | { |
| 88 | - $query->query_fields = "SQL_CALC_FOUND_ROWS DISTINCT(`".$wpdb->users."`.`ID`)"; | |
| 89 | - $query->query_from = " FROM `".$wpdb->users."`, `".$wpdb->usermeta."`"; // Include meta table also. | |
| 90 | - $query->query_where = " WHERE `".$wpdb->users."`.`ID` = `".$wpdb->usermeta."`.`user_id`"; // Join w/ meta table. | |
| 91 | - $query->query_where .= " AND (".apply_filters("ws_plugin__s2member_before_users_list_search_where_or_before", "", get_defined_vars()); | |
| 92 | - $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."')"; | |
| 93 | - $query->query_where .= " OR ((`".$wpdb->usermeta."`.`meta_key` = 'first_name' OR `".$wpdb->usermeta."`.`meta_key` = 'last_name') AND `".$wpdb->usermeta."`.`meta_value` LIKE '".$s."')"; | |
| 94 | - $query->query_where .= " OR (`".$wpdb->usermeta."`.`meta_key` = '".$wpdb->base_prefix."s2member_subscr_id' AND `".$wpdb->usermeta."`.`meta_value` LIKE '".$s."')"; | |
| 95 | - $query->query_where .= " OR (`".$wpdb->usermeta."`.`meta_key` = '".$wpdb->base_prefix."s2member_custom' AND `".$wpdb->usermeta."`.`meta_value` LIKE '".$s."')"; | |
| 96 | - $query->query_where .= " OR (`".$wpdb->usermeta."`.`meta_key` = '".$wpdb->base_prefix."s2member_custom_fields' AND `".$wpdb->usermeta."`.`meta_value` LIKE '".$s."')"; | |
| 97 | - 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. | |
| 98 | - $query->query_where .= " OR (`".$wpdb->usermeta."`.`meta_key` = '".$wpdb->base_prefix."s2member_notes' AND `".$wpdb->usermeta."`.`meta_value` LIKE '".$s."')"; | |
| 99 | - $query->query_where .= apply_filters("ws_plugin__s2member_before_users_list_search_where_or_after", "", get_defined_vars()).")"; // Leaving room for additional searches here. | |
| 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; | |
| 100 | 93 | |
| 101 | - if(is_multisite()) // On a Multisite Network we need to make sure we're searching only users w/ capabilities on this blog. | |
| 102 | - $query->query_where .= " AND `".$wpdb->users."`.`ID` IN(SELECT DISTINCT(`user_id`) FROM `".$wpdb->usermeta."` WHERE `meta_key` = '".$wpdb->prefix."capabilities')"; | |
| 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 .= ")))"; | |
| 103 | 111 | |
| 104 | - $query->query_from = apply_filters("ws_plugin__s2member_before_users_list_search_from", $query->query_from, get_defined_vars()); | |
| 105 | - $query->query_where = apply_filters("ws_plugin__s2member_before_users_list_search_where", $query->query_where, get_defined_vars()); | |
| 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); | |
| 106 | 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 | + } | |
| 107 | 184 | foreach(array_keys(get_defined_vars()) as $__v) $__refs[$__v] =& $$__v; |
| 108 | 185 | do_action("ws_plugin__s2member_after_users_list_search", get_defined_vars()); |
| 109 | 186 | unset($__refs, $__v); |
| 110 | 187 | } |
| @@ -137,8 +214,11 @@ | ||
| 137 | 214 | if(!is_multisite() || !c_ws_plugin__s2member_utils_conds::is_multisite_farm() || is_main_site()) |
| 138 | 215 | $cols["s2member_ccaps"] = "Custom Capabilities"; |
| 139 | 216 | |
| 140 | 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"; | |
| 141 | 221 | |
| 142 | 222 | if($GLOBALS["WS_PLUGIN__"]["s2member"]["o"]["custom_reg_fields"]) |
| 143 | 223 | foreach(json_decode($GLOBALS["WS_PLUGIN__"]["s2member"]["o"]["custom_reg_fields"], TRUE) as $field) |
| 144 | 224 | { |
| @@ -159,8 +239,210 @@ | ||
| 159 | 239 | return apply_filters("ws_plugin__s2member_users_list_cols", $cols, get_defined_vars()); |
| 160 | 240 | } |
| 161 | 241 | |
| 162 | 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 | + /** | |
| 163 | 445 | * Displays column data in the row of details. |
| 164 | 446 | * |
| 165 | 447 | * @package s2Member\Users_List |
| 166 | 448 | * @since 3.5 |
| @@ -212,10 +494,16 @@ | ||
| 212 | 494 | |
| 213 | 495 | $val = (!empty($ccaps)) ? implode("<br />", $ccaps) : "—"; |
| 214 | 496 | } |
| 215 | 497 | else if($col === "s2member_auto_eot_time") |
| 216 | - $val = ($v = get_user_option("s2member_auto_eot_time", $user_id)) ? date("D M jS, Y", (integer)$v)."<br /><small>@ precisely ".date("g:i a", (integer)$v)."</small>" : "—"; | |
| 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>" : "—"; | |
| 217 | 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 | + | |
| 218 | 506 | else if(preg_match("/^s2member_custom_field_/", $col)) |
| 219 | 507 | { |
| 220 | 508 | if(!$last_fields_id || $last_fields_id !== $user_id) |
| 221 | 509 | $fields = get_user_option("s2member_custom_fields", $user_id); |
| @@ -266,14 +554,27 @@ | ||
| 266 | 554 | { |
| 267 | 555 | if(!empty($_REQUEST['s'])) |
| 268 | 556 | return $columns; |
| 269 | 557 | |
| 270 | - $columns['s2member_registration_time'] = 's2member_registration_time'; | |
| 271 | - $columns['s2member_subscr_id'] = 's2member_subscr_id'; | |
| 272 | - $columns['s2member_auto_eot_time'] = 's2member_auto_eot_time'; | |
| 273 | - $columns['s2member_login_counter'] = 's2member_login_counter'; | |
| 274 | - $columns['s2member_last_login_time'] = 's2member_last_login_time'; | |
| 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 | + } | |
| 275 | 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 | + | |
| 276 | 577 | return $columns; |
| 277 | 578 | } |
| 278 | 579 | |
| 279 | 580 | /** |
| @@ -310,8 +611,10 @@ | ||
| 310 | 611 | $query->query_orderby = "ORDER BY `___m`.`meta_value` ".$vars['order']; |
| 311 | 612 | break; |
| 312 | 613 | |
| 313 | 614 | case 's2member_auto_eot_time': |
| 615 | + case 's2member_last_auto_eot_time': | |
| 616 | + case 's2member_last_auto_eot_processed_time': | |
| 314 | 617 | case 's2member_login_counter': |
| 315 | 618 | case 's2member_last_login_time': |
| 316 | 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'])."')"; |
| 317 | 620 | $query->query_orderby = "ORDER BY CAST(`___m`.`meta_value` AS UNSIGNED) ".$vars['order']; |