| @@ -279,8 +279,49 @@ | ||
| 279 | 279 | return (bool)apply_filters('ws_plugin__s2member_allow_eot_user_deletion', FALSE, get_defined_vars()); |
| 280 | 280 | } |
| 281 | 281 | |
| 282 | 282 | /** |
| 283 | + * Applies the configured EOT role demotion to a user. | |
| 284 | + * | |
| 285 | + * @package s2Member\Auto_EOT_System | |
| 286 | + * @since 260916.2004 | |
| 287 | + * | |
| 288 | + * @param \WP_User $user WordPress user being demoted. | |
| 289 | + * @param string $demotion_to_role Role assigned after EOT. | |
| 290 | + * | |
| 291 | + * @return string Role before demotion, for EOT history. | |
| 292 | + */ | |
| 293 | + public static function demote_user_roles($user = NULL, $demotion_to_role = 'subscriber') | |
| 294 | + { | |
| 295 | + if(!is_object($user) || empty($user->ID) || !is_string($demotion_to_role) || !$demotion_to_role) | |
| 296 | + return ''; | |
| 297 | + | |
| 298 | + $existing_role = c_ws_plugin__s2member_user_access::user_access_role($user); | |
| 299 | + //260916.2030 Preserve the historical all-role path exactly; upgraded installations are explicitly seeded to this policy. | |
| 300 | + if($GLOBALS['WS_PLUGIN__']['s2member']['o']['eot_demotion_from'] === 'all') | |
| 301 | + { | |
| 302 | + if($existing_role !== $demotion_to_role) | |
| 303 | + $user->set_role($demotion_to_role); | |
| 304 | + return $existing_role; | |
| 305 | + } | |
| 306 | + | |
| 307 | + $eot_original_role = ''; | |
| 308 | + //260916.2030 The new default removes only explicit paid s2Member Level roles; Subscriber/Level 0 is a possible destination, never a Demote From role. | |
| 309 | + foreach((array)$user->roles as $role) | |
| 310 | + if(preg_match('/^s2member_level[1-9][0-9]*$/', $role)) | |
| 311 | + { | |
| 312 | + if(!$eot_original_role) | |
| 313 | + $eot_original_role = $role; | |
| 314 | + if($role !== $demotion_to_role) | |
| 315 | + $user->remove_role($role); | |
| 316 | + } | |
| 317 | + if(!in_array($demotion_to_role, (array)$user->roles, TRUE)) | |
| 318 | + $user->add_role($demotion_to_role); | |
| 319 | + | |
| 320 | + return $eot_original_role ?: $existing_role; | |
| 321 | + } | |
| 322 | + | |
| 323 | + /** | |
| 283 | 324 | * Records when an End-of-Term action was processed and appends one compact history note. |
| 284 | 325 | * |
| 285 | 326 | * @package s2Member\Auto_EOT_System |
| 286 | 327 | * @since 260822.0653 |
| @@ -347,9 +388,16 @@ | ||
| 347 | 388 | foreach(array($original_role, $destination_role) as $_role) |
| 348 | 389 | { |
| 349 | 390 | $_role = (string)$_role; |
| 350 | 391 | if(preg_match('/^s2member_level([0-9]+)$/', $_role, $_matches)) |
| 351 | - $role_labels[$_role] = 'Level '.(int)$_matches[1]; | |
| 392 | + { | |
| 393 | + //260829.0618 Keep EOT audit notes consistent with the administrator's forced s2Member labels, while retaining concise Level N names when label translation is disabled. | |
| 394 | + $_level = (int)$_matches[1]; | |
| 395 | + $_label_key = 'level'.$_level.'_label'; | |
| 396 | + $role_labels[$_role] = !empty($GLOBALS["WS_PLUGIN__"]["s2member"]["o"]["apply_label_translations"]) && !empty($GLOBALS["WS_PLUGIN__"]["s2member"]["o"][$_label_key]) | |
| 397 | + ? $GLOBALS["WS_PLUGIN__"]["s2member"]["o"][$_label_key] | |
| 398 | + : 'Level '.$_level; | |
| 399 | + } | |
| 352 | 400 | else if($_role === 's2member_pending_deletion') |
| 353 | 401 | $role_labels[$_role] = 'Pending Deletion'; |
| 354 | 402 | else if($_role && isset($wp_roles->roles[$_role]['name'])) |
| 355 | 403 | $role_labels[$_role] = translate_user_role($wp_roles->roles[$_role]['name']); |
| @@ -355,9 +403,9 @@ | ||
| 355 | 403 | $role_labels[$_role] = translate_user_role($wp_roles->roles[$_role]['name']); |
| 356 | 404 | else |
| 357 | 405 | $role_labels[$_role] = $_role ? ucwords(str_replace(array('-', '_'), ' ', $_role)) : 'Unknown Role'; |
| 358 | 406 | } |
| 359 | - unset($_role, $_matches); | |
| 407 | + unset($_role, $_matches, $_level, $_label_key); | |
| 360 | 408 | |
| 361 | 409 | $gateway_labels = array('paypal' => 'PayPal', 'authnet' => 'Authorize.Net', 'clickbank' => 'ClickBank', 'ccbill' => 'ccBill', 'alipay' => 'AliPay', 'google' => 'Google Wallet', 'stripe' => 'Stripe'); |
| 362 | 410 | $gateway_key = strtolower((string)$subscr_gateway); |
| 363 | 411 | $gateway_label = isset($gateway_labels[$gateway_key]) ? $gateway_labels[$gateway_key] : ucwords(str_replace(array('-', '_'), ' ', $gateway_key)); |
| @@ -363,9 +411,13 @@ | ||
| 363 | 411 | $gateway_label = isset($gateway_labels[$gateway_key]) ? $gateway_labels[$gateway_key] : ucwords(str_replace(array('-', '_'), ' ', $gateway_key)); |
| 364 | 412 | $removed_ccaps = array_values(array_unique(array_filter(array_map('strval', (array)$removed_ccaps), 'strlen'))); |
| 365 | 413 | sort($removed_ccaps, SORT_STRING); |
| 366 | 414 | |
| 367 | - $note = $processed_display.' s2Member: Demoted from '.$role_labels[(string)$original_role].' to '.$role_labels[(string)$destination_role]; | |
| 415 | + //260829.0618 Avoid recording a misleading role transition when EOT processing finds the user already in the role selected under Demote To Role. | |
| 416 | + if($original_role === $destination_role) | |
| 417 | + $note = $processed_display.' s2Member: EOT processed, already '.$role_labels[(string)$original_role]; | |
| 418 | + else | |
| 419 | + $note = $processed_display.' s2Member: Demoted from '.$role_labels[(string)$original_role].' to '.$role_labels[(string)$destination_role]; | |
| 368 | 420 | if($removed_ccaps) |
| 369 | 421 | $note .= ' (removed ccaps: '.implode(', ', $removed_ccaps).')'; |
| 370 | 422 | $note .= '.'; |
| 371 | 423 | if($subscr_gateway && $subscr_id) |
| @@ -720,9 +772,10 @@ | ||
| 720 | 772 | { |
| 721 | 773 | if(!function_exists('wp_cron') || !$recurring_at) |
| 722 | 774 | $issues['cron_missing'] = $critical = TRUE; |
| 723 | 775 | else if((int)$recurring_at < $now - HOUR_IN_SECONDS) |
| 724 | - $issues['cron_overdue'] = $critical = TRUE; | |
| 776 | + //260908.2031 An overdue WP-Cron event can be normal on a quiet site; keep it visible as Attention, while missing cron or an actual overdue EOT backlog remain critical. | |
| 777 | + $issues['cron_overdue'] = TRUE; | |
| 725 | 778 | } |
| 726 | 779 | else if($mode === '2' && !empty($state['last_external_completed_at']) && $now - (int)$state['last_external_completed_at'] >= 2 * HOUR_IN_SECONDS) |
| 727 | 780 | $issues['external_cron_stale'] = $critical = TRUE; |
| 728 | 781 | |
| @@ -802,9 +855,10 @@ | ||
| 802 | 855 | $reasons[] = number_format_i18n($health['pending_count']).' End-of-Term action'.($health['pending_count'] === 1 ? ' is' : 's are').' pending; the oldest has been overdue for '.human_time_diff($health['oldest_due_at'], time()).'.'; |
| 803 | 856 | if(in_array('repeated_abandoned', $health['issues'], TRUE)) |
| 804 | 857 | $reasons[] = number_format_i18n($health['consecutive_abandoned_runs']).' consecutive Automatic End-of-Term workers ended without reaching normal completion.'; |
| 805 | 858 | |
| 806 | - $settings_url = admin_url('/admin.php?page=ws-plugin--s2member-paypal-ops').'#ws-plugin--s2member-auto-eot-system-enabled'; | |
| 859 | + //260908.2031 Open the collapsed EOT panel before scrolling to its setting; a hash alone targets a hidden control. | |
| 860 | + $settings_url = admin_url('/admin.php?page=ws-plugin--s2member-paypal-ops&s2member-open-panel=auto-eot').'#ws-plugin--s2member-auto-eot-system-enabled'; | |
| 807 | 861 | $notice = '<strong>s2Member Automatic End-of-Term needs attention.</strong> '.esc_html(implode(' ', $reasons)).' <a href="'.esc_url($settings_url).'">Review Automatic End-of-Term settings</a>.'; |
| 808 | 862 | c_ws_plugin__s2member_admin_notices::display_admin_notice($notice, TRUE); |
| 809 | 863 | } |
| 810 | 864 | |
| @@ -1092,10 +1146,10 @@ | ||
| 1092 | 1146 | do_action('ws_plugin__s2member_during_collective_mods', $user_id, get_defined_vars(), $eot_del_type, 'modification', $demotion_role); |
| 1093 | 1147 | do_action('ws_plugin__s2member_during_collective_eots', $user_id, get_defined_vars(), $eot_del_type, 'modification'); |
| 1094 | 1148 | unset($__refs, $__v); // Housekeeping. |
| 1095 | 1149 | |
| 1096 | - if($existing_role !== $demotion_role /* Only if NOT the existing Role. */) | |
| 1097 | - $user->set_role($demotion_role /* Give User the demotion Role. */); | |
| 1150 | + //260916.2004 New installs replace only the s2Member Level role; upgraded sites retain legacy all-role replacement until the administrator changes it. | |
| 1151 | + $eot_membership_role = self::demote_user_roles($user, $demotion_role); | |
| 1098 | 1152 | |
| 1099 | 1153 | if(apply_filters('ws_plugin__s2member_remove_ccaps_during_eot_events', (bool)$GLOBALS['WS_PLUGIN__']['s2member']['o']['eots_remove_ccaps'], get_defined_vars())) |
| 1100 | 1154 | foreach($user->allcaps as $cap => $cap_enabled) |
| 1101 | 1155 | if(preg_match('/^access_s2member_ccap_/', $cap)) |
| @@ -1133,9 +1187,9 @@ | ||
| 1133 | 1187 | //260822.0653 Record the triggering EOT separately from when this worker actually completed the demotion, using the pre-cleanup role/payment snapshot above. |
| 1134 | 1188 | self::record_eot_history($user_id, array( |
| 1135 | 1189 | 'eot_time' => $auto_eot_time, |
| 1136 | 1190 | 'processed_at' => $processed_at, |
| 1137 | - 'original_role' => $existing_role, | |
| 1191 | + 'original_role' => $eot_membership_role, | |
| 1138 | 1192 | 'destination_role' => $demotion_role, |
| 1139 | 1193 | 'removed_ccaps' => $removed_ccaps, |
| 1140 | 1194 | 'subscr_gateway' => $subscr_gateway, |
| 1141 | 1195 | 'subscr_id' => $subscr_id, |