<?php

namespace DisableXMLRPC;

use WP_PowerSuite\Core\AbstractModule;

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

class Module extends AbstractModule {

	private const MODULE_ID = 'disable-xmlrpc';
	private const MODULE_VERSION = '1.0.4';

	/** Late priority; direct xmlrpc.php blocking is the authoritative control. */
	private const FILTER_PRIORITY_LAST = \PHP_INT_MAX;

	public function getId(): string {
		return self::MODULE_ID;
	}

	public function getName(): string {
		return __( 'Disable XML-RPC', 'wp-powersuite' );
	}

	public function getVersion(): string {
		return self::MODULE_VERSION;
	}

	public function getDescription(): string {
		return __( 'Closes the old XML-RPC channel many password-guessing tools still target. Fine for most sites; skip if you rely on legacy apps or remote publishing that need it.', 'wp-powersuite' );
	}

	public function getContext(): string|array {
		return 'frontend';
	}

	protected function registerHooks(): void {
		if ( $this->isDirectXmlrpcPhpRequest() && ! $this->isRequestAllowed() ) {
			$this->sendXmlrpcForbidden();
		}

		add_filter( 'xmlrpc_enabled', [ $this, 'filterXmlrpcEnabled' ], self::FILTER_PRIORITY_LAST );
		add_filter( 'xmlrpc_methods', [ $this, 'filterXmlrpcMethods' ], self::FILTER_PRIORITY_LAST );

		if ( ! is_admin() ) {
			remove_action( 'wp_head', 'rsd_link' );
			remove_action( 'wp_head', 'wlwmanifest_link' );
			add_filter( 'wp_headers', [ $this, 'removeXmlrpcPingbackHeader' ], 10, 1 );
		}
	}

	/**
	 * @param bool $enabled Whether XML-RPC is enabled.
	 */
	public function filterXmlrpcEnabled( $enabled ): bool {
		if ( $this->isRequestAllowed() ) {
			return (bool) $enabled;
		}

		return false;
	}

	/**
	 * @param array<string, callable> $methods Registered XML-RPC methods.
	 * @return array<string, callable>
	 */
	public function filterXmlrpcMethods( array $methods ): array {
		if ( $this->isRequestAllowed() ) {
			return $methods;
		}

		return [];
	}

	public function removeXmlrpcPingbackHeader( array $headers ): array {
		unset( $headers['X-Pingback'], $headers['x-pingback'] );
		return $headers;
	}

	private function sendXmlrpcForbidden(): void {
		$this->ensureTextdomainLoaded();

		status_header( 403 );
		nocache_headers();

		$charset = (string) get_option( 'blog_charset' );
		if ( '' === $charset ) {
			$charset = 'utf-8';
		}
		header( 'Content-Type: text/xml; charset=' . $charset );

		$message = htmlspecialchars(
			__( 'XML-RPC services are disabled.', 'wp-powersuite' ),
			ENT_XML1 | ENT_COMPAT,
			'UTF-8'
		);

		echo '<?xml version="1.0" encoding="utf-8"?' . ">\n";
		echo "<response>\n";
		echo "<error>1</error>\n";
		echo '<message>' . $message . "</message>\n";
		echo "</response>\n";
		exit;
	}

	private function ensureTextdomainLoaded(): void {
		if ( is_textdomain_loaded( 'wp-powersuite' ) ) {
			return;
		}

		if ( ! function_exists( 'load_plugin_textdomain' ) || ! defined( 'WP_POWERSUITE_PLUGIN_BASENAME' ) ) {
			return;
		}

		load_plugin_textdomain(
			'wp-powersuite',
			false,
			dirname( WP_POWERSUITE_PLUGIN_BASENAME ) . '/languages'
		);
	}

	private function isRequestAllowed(): bool {
		/**
		 * Allow specific XML-RPC consumers to bypass this module (e.g. Jetpack integrations).
		 *
		 * @param bool $allow Default false — block unless an integration returns true.
		 */
		return (bool) apply_filters( 'wpps_disable_xmlrpc_allow_request', false );
	}

	private function isDirectXmlrpcPhpRequest(): bool {
		// Core sets this in xmlrpc.php before wp-load.php (works when SCRIPT_FILENAME/REQUEST_URI lie).
		if ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) {
			return true;
		}

		if ( isset( $_SERVER['SCRIPT_FILENAME'] ) ) {
			$script = wp_basename( wp_unslash( $_SERVER['SCRIPT_FILENAME'] ) );
			if ( 'xmlrpc.php' === $script ) {
				return true;
			}
		}

		if ( isset( $_SERVER['REQUEST_URI'] ) ) {
			$uri  = wp_unslash( $_SERVER['REQUEST_URI'] );
			$path = strtok( $uri, '?' );
			if ( is_string( $path ) && $this->requestPathIsXmlrpc( $path ) ) {
				return true;
			}
		}

		return false;
	}

	private function requestPathIsXmlrpc( string $path ): bool {
		if ( preg_match( '#/xmlrpc\.php(?:$|[?/])#', $path ) ) {
			return true;
		}

		// Extensionless routes some proxies/CDNs rewrite (e.g. /xmlrpc).
		return (bool) preg_match( '#/xmlrpc(?:$|[?/])#', $path );
	}
}
