<?php
/**
 * Sanitization file.
 *
 * @package Activitypub
 */

namespace Activitypub;

use Activitypub\Collection\Remote_Actors;
use Activitypub\Model\Blog;

/**
 * Sanitization class.
 */
class Sanitize {
	/**
	 * Sanitize a list of URLs.
	 *
	 * @param string|array $value The value to sanitize.
	 * @return array The sanitized list of URLs.
	 */
	public static function url_list( $value ) {
		if ( ! \is_array( $value ) ) {
			$value = \explode( PHP_EOL, (string) $value );
		}

		$value = \array_filter( $value );
		$value = \array_map( 'trim', $value );
		$value = \array_map( 'sanitize_url', $value );
		$value = \array_unique( $value );

		return \array_values( $value );
	}

	/**
	 * Sanitize and normalize a list of account identifiers to ActivityPub IDs.
	 *
	 * This function processes various identifier formats, such as URLs and
	 * webfinger identifiers, and normalizes them into a consistent format.
	 *
	 * @param string|array $value The value to sanitize.
	 *
	 * @return array The sanitized and normalized list of account identifiers.
	 */
	public static function identifier_list( $value ) {
		if ( ! \is_array( $value ) ) {
			$value = \explode( PHP_EOL, (string) $value );
		}

		$value = \array_filter( $value );
		$uris  = array();

		foreach ( $value as $uri ) {
			$uri = \trim( $uri );
			$uri = \ltrim( $uri, '@' );

			if ( \is_email( $uri ) ) {
				$_uri = Webfinger::resolve( $uri );
				if ( \is_wp_error( $_uri ) ) {
					$uris[] = $uri;
					continue;
				}

				$uri = $_uri;
			}

			$uri   = \sanitize_url( $uri );
			$actor = Remote_Actors::fetch_by_uri( $uri );
			if ( \is_wp_error( $actor ) ) {
				$uris[] = $uri;
			} else {
				$uris[] = \sanitize_url( $actor->guid );
			}
		}

		return \array_values( \array_unique( $uris ) );
	}

	/**
	 * Sanitize a list of hosts.
	 *
	 * @param string $value The value to sanitize.
	 * @return string The sanitized list of hosts.
	 */
	public static function host_list( $value ) {
		$value = \explode( PHP_EOL, (string) $value );
		$value = \array_map(
			static function ( $host ) {
				$host = \trim( $host );
				$host = \strtolower( $host );
				$host = \set_url_scheme( $host );
				$host = \sanitize_url( $host, array( 'http', 'https' ) );

				// Remove protocol.
				if ( \str_contains( $host, 'http' ) ) {
					$host = \wp_parse_url( $host, PHP_URL_HOST );
				}

				return \filter_var( $host, FILTER_VALIDATE_DOMAIN );
			},
			$value
		);

		return \implode( PHP_EOL, \array_filter( $value ) );
	}

	/**
	 * Sanitize a blog identifier.
	 *
	 * @param string $value The value to sanitize.
	 * @return string The sanitized blog identifier.
	 */
	public static function blog_identifier( $value ) {
		// Hack to allow dots in the username.
		$parts     = \explode( '.', (string) $value );
		$sanitized = \array_map( 'sanitize_title', $parts );
		$sanitized = \implode( '.', $sanitized );

		if ( empty( $sanitized ) ) {
			return Blog::get_default_username();
		}

		// Check for login or nicename.
		$user = new \WP_User_Query(
			array(
				'search'         => $sanitized,
				'search_columns' => array( 'user_login', 'user_nicename' ),
				'number'         => 1,
				'hide_empty'     => true,
				'fields'         => 'ID',
			)
		);

		if ( $user->get_results() ) {
			\add_settings_error(
				'activitypub_blog_identifier',
				'activitypub_blog_identifier',
				\esc_html__( 'You cannot use an existing author&#8217;s name for the blog profile ID.', 'activitypub' )
			);

			return Blog::get_default_username();
		}

		return $sanitized;
	}

	/**
	 * Get the sanitized value of a constant.
	 *
	 * @param mixed $value The constant value.
	 *
	 * @return string The sanitized value.
	 */
	public static function constant_value( $value ) {
		if ( is_bool( $value ) ) {
			return $value ? 'true' : 'false';
		}

		if ( is_string( $value ) ) {
			return esc_attr( $value );
		}

		if ( is_array( $value ) ) {
			// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
			return print_r( $value, true );
		}

		return $value;
	}

	/**
	 * Sanitize a webfinger identifier.
	 *
	 * @param string $value The value to sanitize.
	 *
	 * @return string The sanitized webfinger identifier.
	 */
	public static function webfinger( $value ) {
		$value = \str_replace( 'acct:', '', $value );
		$value = \trim( $value, '@' );

		return $value;
	}

	/**
	 * Sanitize content for ActivityPub.
	 *
	 * @param string $content The content to convert.
	 *
	 * @return string The converted content.
	 */
	public static function content( $content ) {
		// Only make URLs clickable if no anchor tags exist, to avoid corrupting existing links.
		if ( false === \strpos( $content, '<a ' ) ) {
			$content = \make_clickable( $content );
		}

		$content = \wpautop( $content );
		$content = \wp_kses_post( $content );

		return $content;
	}

	/**
	 * Strip whitespace between HTML tags.
	 *
	 * Removes newlines, carriage returns, and tabs that appear between HTML tags,
	 * preserving whitespace within text content and preformatted elements.
	 *
	 * @param string $content The content to process.
	 *
	 * @return string The content with whitespace between tags removed.
	 */
	public static function strip_whitespace( $content ) {
		return \trim( \preg_replace( '/>[\n\r\t]+</', '><', $content ) );
	}

	/**
	 * Clean HTML for ActivityPub federation.
	 *
	 * Keeps all WordPress allowed tags but removes global attributes like
	 * class, id, style, data-*, aria-* that increase payload size.
	 *
	 * @see https://github.com/Automattic/wordpress-activitypub/issues/2619
	 *
	 * @param string $content The HTML content to clean.
	 *
	 * @return string The cleaned HTML content.
	 */
	public static function clean_html( $content ) {
		if ( empty( $content ) ) {
			return $content;
		}

		// Start with all WordPress allowed post tags.
		$allowed_html = \wp_kses_allowed_html( 'post' );

		// Global attributes to remove from all elements.
		$remove_attrs = array(
			'aria-controls',
			'aria-current',
			'aria-describedby',
			'aria-details',
			'aria-expanded',
			'aria-hidden',
			'aria-label',
			'aria-labelledby',
			'aria-live',
			'class',
			'data-*',
			'decoding',
			'dir',
			'hidden',
			'id',
			'lang',
			'loading',
			'role',
			'style',
			'tabindex',
			'title',
			'xml:lang',
		);

		/**
		 * Filter the global attributes to remove from all elements.
		 *
		 * @param array $remove_attrs Global attributes to remove.
		 */
		$remove_attrs = \apply_filters( 'activitypub_remove_html_attributes', $remove_attrs );

		// Remove global attributes from all tags.
		foreach ( $allowed_html as $tag => $attrs ) {
			$allowed_html[ $tag ] = \array_diff_key( $attrs, \array_flip( $remove_attrs ) );
		}

		// Re-add class and title for anchors (needed for microformats).
		$allowed_html['a']['class'] = true;
		$allowed_html['a']['title'] = true;

		// Re-add class for spans (needed for microformats).
		$allowed_html['span']['class'] = true;

		/**
		 * Filter the final allowed HTML for ActivityPub content.
		 *
		 * @param array $allowed_html The allowed HTML structure for wp_kses.
		 */
		$allowed_html = \apply_filters( 'activitypub_allowed_html', $allowed_html );

		return \wp_kses( $content, $allowed_html, \wp_allowed_protocols() );
	}
}
