<?php

defined( 'ABSPATH' ) || exit;

/**
 * Security Hardening module.
 *
 * Bundles five high-impact, low-complexity WordPress security toggles into
 * one module so a typical Bowden Works deployment can enable safe defaults
 * with five checkboxes:
 *
 *  1. block_user_enumeration  — block /?author=N redirects (which reveal
 *     usernames) for guests, and remove the /wp-json/wp/v2/users REST endpoint
 *     for unauthenticated requests.
 *  2. disable_xmlrpc           — turn off XML-RPC entirely (xmlrpc.php returns
 *     disabled, X-Pingback header removed, RSD/WLW <link>s removed).
 *  3. disable_file_edit        — define DISALLOW_FILE_EDIT so a compromised
 *     admin account cannot drop a webshell via Appearance → Theme/Plugin Editor.
 *  4. strip_version            — remove the WP version from <meta generator>,
 *     RSS feed generators, and `?ver=` parameters on core asset URLs so
 *     attackers cannot pattern-match vulnerable versions.
 *  5. disable_app_passwords    — disable the WordPress Application Passwords
 *     feature (introduced in WP 5.6) when it's not in use, removing both the
 *     UI and the REST authentication path.
 *
 * Each toggle is independent — turn on whatever fits the site. Defaults to
 * everything OFF on first install so admins enable each one deliberately.
 *
 * @package BW_Dev
 */

class BW_Dev_Module_Security_Hardening implements BW_Dev_Module_Interface {

	/**
	 * All toggles default to ON — the safe-baseline posture for a typical
	 * Bowden Works deployment. Sites that need XML-RPC (legacy mobile clients)
	 * or Application Passwords (headless WP) can deliberately uncheck those
	 * toggles in the settings tab.
	 */
	private const TOGGLES = array(
		'block_user_enumeration' => true,
		'disable_xmlrpc'         => true,
		'disable_file_edit'      => true,
		'strip_version'          => true,
		'disable_app_passwords'  => true,
	);

	public function slug(): string {
		return 'security_hardening';
	}

	public function label(): string {
		return __( 'Security Hardening', 'bw-dev' );
	}

	public function group(): string {
		return 'security';
	}

	public function default_settings(): array {
		return self::TOGGLES;
	}

	public function sanitize( array $data ): array {
		$out = array();
		foreach ( self::TOGGLES as $key => $default ) {
			$out[ $key ] = ! empty( $data[ $key ] );
		}
		return $out;
	}

	public function register(): void {
		if ( $this->is_on( 'block_user_enumeration' ) ) {
			add_action( 'parse_request',  array( $this, 'block_author_enumeration' ), 1 );
			add_filter( 'rest_endpoints', array( $this, 'remove_users_rest_endpoint' ) );
		}

		if ( $this->is_on( 'disable_xmlrpc' ) ) {
			add_filter( 'xmlrpc_enabled', '__return_false' );
			add_filter( 'wp_headers',     array( $this, 'remove_pingback_header' ) );
			remove_action( 'wp_head', 'rsd_link' );
			remove_action( 'wp_head', 'wlwmanifest_link' );
		}

		if ( $this->is_on( 'disable_file_edit' ) && ! defined( 'DISALLOW_FILE_EDIT' ) ) {
			define( 'DISALLOW_FILE_EDIT', true );
		}

		if ( $this->is_on( 'strip_version' ) ) {
			remove_action( 'wp_head', 'wp_generator' );
			add_filter( 'the_generator',     '__return_empty_string' );
			add_filter( 'style_loader_src',  array( $this, 'strip_wp_version_query' ), 9999 );
			add_filter( 'script_loader_src', array( $this, 'strip_wp_version_query' ), 9999 );
		}

		if ( $this->is_on( 'disable_app_passwords' ) ) {
			add_filter( 'wp_is_application_passwords_available', '__return_false' );
		}
	}

	private function is_on( string $key ): bool {
		$value = bw_dev()->settings()->get( $this->slug(), $key, self::TOGGLES[ $key ] ?? false );
		return (bool) $value;
	}

	/* ---------------------------------------------------------------------
	 * 1. Block user enumeration
	 * ------------------------------------------------------------------- */

	public function block_author_enumeration(): void {
		if ( is_user_logged_in() ) {
			// Logged-in users (admins, editors) get the normal author redirect for tooling.
			return;
		}
		// Skip in admin and REST/AJAX so backend tooling that legitimately uses ?author=
		// (e.g. the user list table filters) keeps working.
		if ( is_admin() || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) || wp_doing_ajax() ) {
			return;
		}
		// Read-only check on a public query parameter — no nonce surface here.
		$query = wp_unslash( $_GET ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$has_author = ( isset( $query['author'] ) && '' !== $query['author'] )
			|| ( isset( $query['author_name'] ) && '' !== $query['author_name'] );
		if ( ! $has_author ) {
			return;
		}
		wp_safe_redirect( home_url( '/' ), 302 );
		exit;
	}

	/**
	 * Drop the /wp-json/wp/v2/users routes from the REST API for guests so
	 * scripts can't enumerate accounts via the public REST surface.
	 *
	 * @param array $endpoints Map of route → endpoint definition.
	 */
	public function remove_users_rest_endpoint( $endpoints ) {
		if ( is_user_logged_in() ) {
			return $endpoints;
		}
		if ( ! is_array( $endpoints ) ) {
			return $endpoints;
		}
		if ( isset( $endpoints['/wp/v2/users'] ) ) {
			unset( $endpoints['/wp/v2/users'] );
		}
		// Match `/wp/v2/users/(?P<id>...)` and any sibling key holding it.
		foreach ( array_keys( $endpoints ) as $route ) {
			if ( 0 === strpos( (string) $route, '/wp/v2/users/' ) ) {
				unset( $endpoints[ $route ] );
			}
		}
		return $endpoints;
	}

	/* ---------------------------------------------------------------------
	 * 2. Disable XML-RPC
	 * ------------------------------------------------------------------- */

	public function remove_pingback_header( $headers ) {
		if ( is_array( $headers ) ) {
			unset( $headers['X-Pingback'] );
		}
		return $headers;
	}

	/* ---------------------------------------------------------------------
	 * 4. Strip WP version fingerprints
	 *
	 * Only strips `?ver=` when it equals the actual WordPress version, so
	 * plugin-supplied ver= cache busters are preserved.
	 * ------------------------------------------------------------------- */

	public function strip_wp_version_query( $src ) {
		if ( ! is_string( $src ) || '' === $src ) {
			return $src;
		}
		$wp_version = get_bloginfo( 'version' );
		if ( '' === $wp_version ) {
			return $src;
		}
		$needle = 'ver=' . rawurlencode( $wp_version );
		if ( false === strpos( $src, $needle ) && false === strpos( $src, 'ver=' . $wp_version ) ) {
			return $src;
		}
		return remove_query_arg( 'ver', $src );
	}

	/* ---------------------------------------------------------------------
	 * Settings tab
	 * ------------------------------------------------------------------- */

	public function render_tab(): void {
		$prefix    = BW_Dev_Settings::OPTION . '[' . $this->slug() . ']';
		$constant_warning = defined( 'DISALLOW_FILE_EDIT' ) && DISALLOW_FILE_EDIT;
		?>
		<p class="description">
			<?php esc_html_e( 'Five independent security toggles. Each is OFF by default — enable the ones that fit this site.', 'bw-dev' ); ?>
		</p>

		<table class="form-table" role="presentation">
			<tbody>
				<?php $this->render_toggle_row(
					'block_user_enumeration',
					__( 'Block user enumeration', 'bw-dev' ),
					__( 'Stops the two main ways attackers harvest WordPress usernames before brute-forcing: /?author=N redirects and /wp-json/wp/v2/users for guests. Logged-in users are unaffected.', 'bw-dev' ),
					$prefix
				); ?>

				<?php $this->render_toggle_row(
					'disable_xmlrpc',
					__( 'Disable XML-RPC', 'bw-dev' ),
					__( 'Turns off /xmlrpc.php and removes the X-Pingback header and RSD link tags. Used heavily in pingback-based DDoS and credential brute-force attacks; rarely used legitimately in 2026 outside Jetpack and a few mobile clients.', 'bw-dev' ),
					$prefix
				); ?>

				<?php $this->render_toggle_row(
					'disable_file_edit',
					__( 'Disable file editing in admin', 'bw-dev' ),
					sprintf(
						/* translators: %s: PHP constant name */
						__( 'Defines %s so the Plugin Editor and Theme Editor are removed from wp-admin. If a client admin account is compromised, the attacker cannot drop a webshell by editing theme files in the dashboard.', 'bw-dev' ),
						'<code>DISALLOW_FILE_EDIT</code>'
					),
					$prefix,
					$constant_warning ? __( 'Already defined in wp-config.php — this toggle is redundant but harmless.', 'bw-dev' ) : ''
				); ?>

				<?php $this->render_toggle_row(
					'strip_version',
					__( 'Strip WordPress version fingerprints', 'bw-dev' ),
					__( 'Removes the WP version from the &lt;meta generator&gt; tag, RSS feeds, and ?ver= query parameters on core scripts/styles. Plugin-provided ?ver= (used for cache busting) is preserved. Cosmetic but reduces fingerprinting.', 'bw-dev' ),
					$prefix
				); ?>

				<?php $this->render_toggle_row(
					'disable_app_passwords',
					__( 'Disable Application Passwords', 'bw-dev' ),
					__( 'Turns off the Application Passwords feature added in WP 5.6 (used by some headless setups and the WordPress mobile app). Removes the UI and the REST authentication path. Leave OFF if you know any integration relies on it.', 'bw-dev' ),
					$prefix
				); ?>
			</tbody>
		</table>
		<?php
	}

	private function render_toggle_row( string $key, string $label, string $description, string $prefix, string $extra_note = '' ): void {
		$id      = 'bw-dev-sh-' . sanitize_html_class( $key );
		$name    = $prefix . '[' . $key . ']';
		$checked = $this->is_on( $key );
		?>
		<tr>
			<th scope="row"><?php echo esc_html( $label ); ?></th>
			<td>
				<label for="<?php echo esc_attr( $id ); ?>">
					<input type="checkbox" id="<?php echo esc_attr( $id ); ?>" name="<?php echo esc_attr( $name ); ?>" value="1" <?php checked( $checked ); ?> />
					<?php esc_html_e( 'Enabled', 'bw-dev' ); ?>
				</label>
				<p class="description"><?php echo wp_kses( $description, array( 'code' => array() ) ); ?></p>
				<?php if ( '' !== $extra_note ) : ?>
					<p class="description" style="color:#646970;font-style:italic;"><?php echo esc_html( $extra_note ); ?></p>
				<?php endif; ?>
			</td>
		</tr>
		<?php
	}

	public function uninstall(): void {
		// Settings live under bw_dev_settings — root option drop covers them.
	}
}
