<?php
/** Server-side validation for Live Editor content and block mutations. */

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

/** Dekodiert Verschleierungen wiederholt, damit Sicherheitsprüfungen die kanonische Form sehen. */
function wpagent_companion_live_editor_canonical_string( string $value ): string {
	$canonical = $value;
	for ( $iteration = 0; $iteration < 32; $iteration++ ) {
		$canonical = preg_replace_callback(
			'/&#x([0-9a-f]{1,6})(?![0-9a-f]);?/i',
			static fn( array $match ): string => html_entity_decode( '&#x' . $match[1] . ';', ENT_QUOTES | ENT_HTML5, 'UTF-8' ),
			$canonical
		) ?? $canonical;
		$canonical = preg_replace_callback(
			'/&#([0-9]{1,7})(?![0-9]);?/',
			static fn( array $match ): string => html_entity_decode( '&#' . $match[1] . ';', ENT_QUOTES | ENT_HTML5, 'UTF-8' ),
			$canonical
		) ?? $canonical;
		$canonical = preg_replace( '/&(lt|gt|quot|apos)(?![a-z0-9]);?/i', '&$1;', $canonical ) ?? $canonical;
		$decoded = html_entity_decode( rawurldecode( $canonical ), ENT_QUOTES | ENT_HTML5, 'UTF-8' );
		if ( $decoded === $canonical ) {
			break;
		}
		$canonical = $decoded;
	}
	return $canonical;
}

function wpagent_companion_live_editor_is_sensitive_key( string $key ): bool {
	$normalized = strtolower( preg_replace( '/([a-z0-9])([A-Z])/', '$1_$2', $key ) ?? $key );
	$normalized = trim( preg_replace( '/[^a-z0-9]+/', '_', $normalized ) ?? '', '_' );
	return in_array( $normalized, array( 'token', 'apikey', 'api_token', 'auth', 'auth_token', 'session_token', 'id_token', 'nonce', 'jwt' ), true )
		|| 1 === preg_match( '/(?:^|_)(?:secret|password|passwd|api_key|private_key|credential|authorization|cookie|access_token|refresh_token|bearer_token|csrf_token|license_key|consumer_key)(?:$|_)/', $normalized );
}

/** @return true|WP_Error */
function wpagent_companion_live_editor_validate_active_value( $value, int $depth = 0 ) {
	if ( $depth > 12 ) {
		return new WP_Error( 'wpagent_live_editor_active_depth', __( 'Aktive Blockdaten sind zu tief verschachtelt.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	if ( is_string( $value ) ) {
		$canonical = wpagent_companion_live_editor_canonical_string( $value );
		// Jede erfolgreiche Dekodierung verkürzt die Eingabe. Nach 32 Runden
		// verbleibende Verschleierung ist daher kein legitimer Inhalt, sondern ein
		// bewusst extrem tief kodierter Prüfungs-Bypass und wird fail-closed
		// abgelehnt, statt nur teilweise kanonisiert weitergeprüft zu werden.
		$decoded_once_more = html_entity_decode( rawurldecode( $canonical ), ENT_QUOTES | ENT_HTML5, 'UTF-8' );
		$compact   = preg_replace( '/[\x00-\x20\x7f]+/u', '', $canonical );
		if ( ! is_string( $compact )
			|| $decoded_once_more !== $canonical
			|| preg_match( '/<\s*(?:script|iframe|object|embed|link|meta|style)\b/i', $canonical )
			|| preg_match( '/<[^>]*[\s\/]on[a-z][a-z0-9_-]*\s*=/i', $canonical )
			|| preg_match( '/\bsrcdoc\s*=/i', $canonical )
			|| preg_match( '/(?:javascript|vbscript):/i', $compact )
			|| preg_match( '/data:(?:text\/html|image\/svg\+xml|application\/xhtml\+xml)/i', $compact )
			|| preg_match( '/(?:file:\/\/|blob:https?:)/i', $compact ) ) {
			return new WP_Error( 'wpagent_live_editor_active_content', __( 'Aktive Blockdaten enthalten ausführbaren oder unsicheren Inhalt.', 'wp-agent-companion' ), array( 'status' => 400 ) );
		}
		return true;
	}
	if ( is_array( $value ) ) {
		if ( count( $value ) > 500 ) {
			return new WP_Error( 'wpagent_live_editor_active_size', __( 'Ein Array aktiver Blockdaten ist zu groß.', 'wp-agent-companion' ), array( 'status' => 413 ) );
		}
		foreach ( $value as $key => $item ) {
			$normalized_key = strtolower( preg_replace( '/[^a-z0-9]+/i', '', (string) $key ) ?? '' );
			if ( preg_match( '/^on[a-z0-9_]+$/', $normalized_key )
				|| in_array( $normalized_key, array( 'srcdoc', 'innerhtml', 'outerhtml', 'dangerouslysetinnerhtml', 'proto', 'prototype', 'constructor' ), true )
				|| wpagent_companion_live_editor_is_sensitive_key( (string) $key ) ) {
				return new WP_Error( 'wpagent_live_editor_active_key', __( 'Aktive Blockdaten enthalten einen unsicheren Schlüssel.', 'wp-agent-companion' ), array( 'status' => 400 ) );
			}
			$valid = wpagent_companion_live_editor_validate_active_value( $item, $depth + 1 );
			if ( is_wp_error( $valid ) ) {
				return $valid;
			}
		}
	}
	return true;
}

function wpagent_companion_live_editor_value_matches_type( $value, string $type ): bool {
	if ( 'string' === $type ) return is_string( $value );
	if ( 'number' === $type ) return is_int( $value ) || is_float( $value );
	if ( 'integer' === $type ) return is_int( $value );
	if ( 'boolean' === $type ) return is_bool( $value );
	if ( 'array' === $type ) return is_array( $value ) && ( array() === $value || array_is_list( $value ) );
	if ( 'object' === $type ) return is_array( $value ) && ( array() === $value || ! array_is_list( $value ) );
	if ( 'null' === $type ) return null === $value;
	return false;
}

/** @return true|WP_Error */
function wpagent_companion_live_editor_validate_schema_value( string $path, $value, array $schema ) {
	$types = is_array( $schema['type'] ?? null ) ? $schema['type'] : ( is_string( $schema['type'] ?? null ) ? array( $schema['type'] ) : array() );
	if ( $types && ! array_filter( $types, static fn( $type ): bool => is_string( $type ) && wpagent_companion_live_editor_value_matches_type( $value, $type ) ) ) {
		/* translators: %s: block attribute path. */
		return new WP_Error( 'wpagent_live_editor_attribute_type', sprintf( __( '%s entspricht nicht dem registrierten Blockschema.', 'wp-agent-companion' ), $path ), array( 'status' => 400 ) );
	}
	if ( is_array( $schema['enum'] ?? null ) ) {
		$matched = false;
		foreach ( $schema['enum'] as $candidate ) {
			if ( $candidate === $value ) {
				$matched = true;
				break;
			}
		}
		if ( ! $matched ) {
			/* translators: %s: block attribute path. */
			return new WP_Error( 'wpagent_live_editor_attribute_enum', sprintf( __( '%s gehört nicht zu den registrierten Werten.', 'wp-agent-companion' ), $path ), array( 'status' => 400 ) );
		}
	}
	if ( is_array( $value ) && array_is_list( $value ) && is_array( $schema['items'] ?? null ) ) {
		foreach ( $value as $index => $item ) {
			$valid = wpagent_companion_live_editor_validate_schema_value( $path . '[' . $index . ']', $item, $schema['items'] );
			if ( is_wp_error( $valid ) ) return $valid;
		}
	}
	if ( is_array( $value ) && ! array_is_list( $value ) && is_array( $schema['properties'] ?? null ) ) {
		foreach ( $value as $key => $item ) {
			if ( isset( $schema['properties'][ $key ] ) && is_array( $schema['properties'][ $key ] ) ) {
				$valid = wpagent_companion_live_editor_validate_schema_value( $path . '.' . $key, $item, $schema['properties'][ $key ] );
				if ( is_wp_error( $valid ) ) return $valid;
			} elseif ( false === ( $schema['additionalProperties'] ?? null ) ) {
				/* translators: %s: block attribute path. */
				return new WP_Error( 'wpagent_live_editor_attribute_schema', sprintf( __( '%s gehört nicht zum registrierten Blockschema.', 'wp-agent-companion' ), $path . '.' . $key ), array( 'status' => 400 ) );
			}
		}
	}
	return true;
}

/** @return true|WP_Error */
function wpagent_companion_live_editor_validate_block( array $block, int $depth, array &$counter, bool $enforce_namespace = true ) {
	if ( $depth > WPAGENT_COMPANION_LIVE_EDITOR_MAX_ACTIVE_DEPTH || ++$counter['count'] > WPAGENT_COMPANION_LIVE_EDITOR_MAX_ACTIVE_BLOCKS ) {
		return new WP_Error( 'wpagent_live_editor_active_size', __( 'Die aktive Blockstruktur ist zu groß oder zu tief verschachtelt.', 'wp-agent-companion' ), array( 'status' => 413 ) );
	}
	$name = is_string( $block['name'] ?? null ) ? $block['name'] : '';
	if ( ( $enforce_namespace && ! preg_match( '#^(?:core|kadence|generateblocks|uagb)/[a-z0-9][a-z0-9-]{0,63}$#D', $name ) )
		|| ! preg_match( '#^[a-z0-9][a-z0-9-]{0,63}/[a-z0-9][a-z0-9-]{0,63}$#D', $name )
		|| in_array( $name, array( 'core/html', 'core/freeform', 'core/shortcode' ), true ) ) {
		return new WP_Error( 'wpagent_live_editor_block_forbidden', __( 'Die aktive Blockstruktur enthält einen nicht freigegebenen Blocktyp.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$type = class_exists( 'WP_Block_Type_Registry' ) ? WP_Block_Type_Registry::get_instance()->get_registered( $name ) : null;
	if ( ! $type instanceof WP_Block_Type ) {
		return new WP_Error( 'wpagent_live_editor_block_unregistered', __( 'Ein aktiver Blocktyp ist auf dem Server nicht registriert.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$attributes = is_array( $block['attributes'] ?? null ) ? $block['attributes'] : array();
	$active     = wpagent_companion_live_editor_validate_active_value( $attributes );
	if ( is_wp_error( $active ) ) return $active;
	$schema = is_array( $type->attributes ) ? $type->attributes : array();
	foreach ( $attributes as $key => $value ) {
		if ( ! is_string( $key ) || ! isset( $schema[ $key ] ) || ! is_array( $schema[ $key ] ) ) {
			return new WP_Error( 'wpagent_live_editor_attribute_schema', __( 'Ein Blockattribut gehört nicht zum registrierten Blockschema.', 'wp-agent-companion' ), array( 'status' => 400 ) );
		}
		$valid = wpagent_companion_live_editor_validate_schema_value( $name . '.' . $key, $value, $schema[ $key ] );
		if ( is_wp_error( $valid ) ) return $valid;
	}
	$inner = $block['inner_blocks'] ?? array();
	if ( ! is_array( $inner ) || ! array_is_list( $inner ) ) {
		return new WP_Error( 'wpagent_live_editor_active_blocks', __( 'Die aktive innere Blockstruktur ist ungültig.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	foreach ( $inner as $child ) {
		if ( ! is_array( $child ) ) return new WP_Error( 'wpagent_live_editor_active_blocks', __( 'Ein aktiver innerer Block ist ungültig.', 'wp-agent-companion' ), array( 'status' => 400 ) );
		$valid = wpagent_companion_live_editor_validate_block( $child, $depth + 1, $counter, $enforce_namespace );
		if ( is_wp_error( $valid ) ) return $valid;
	}
	return true;
}

/** @return true|WP_Error */
function wpagent_companion_live_editor_validate_active_content( array $session, array $command, array $active_content ) {
	$encoded = wp_json_encode( $active_content );
	if ( ! is_string( $encoded ) || strlen( $encoded ) > WPAGENT_COMPANION_LIVE_EDITOR_MAX_ACTIVE_BYTES ) {
		return new WP_Error( 'wpagent_live_editor_active_size', __( 'Die aktive Editorstruktur ist größer als 10 MB.', 'wp-agent-companion' ), array( 'status' => 413 ) );
	}
	$action = (string) ( $command['action'] ?? '' );
	$args   = is_array( $command['args'] ?? null ) ? $command['args'] : array();
	$active = wpagent_companion_live_editor_validate_active_value( $args );
	if ( is_wp_error( $active ) ) return $active;
	if ( 'open-post' === $action ) return true;

	$blocks = $active_content['blocks'] ?? null;
	if ( ! is_array( $blocks ) || ! array_is_list( $blocks ) ) {
		return new WP_Error( 'wpagent_live_editor_active_blocks', __( 'Die vollständige aktive Blockstruktur fehlt.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$executor = wpagent_companion_live_editor_executor( $session );
	if ( is_wp_error( $executor ) ) return $executor;
	$previous_user_id = get_current_user_id();
	wp_set_current_user( $executor->ID );
	try {
		$counter = array( 'count' => 0 );
		foreach ( $blocks as $block ) {
			if ( ! is_array( $block ) ) return new WP_Error( 'wpagent_live_editor_active_blocks', __( 'Ein aktiver Block ist ungültig.', 'wp-agent-companion' ), array( 'status' => 400 ) );
			$valid = wpagent_companion_live_editor_validate_block( $block, 0, $counter, false );
			if ( is_wp_error( $valid ) ) return $valid;
		}
		if ( 'create-block' === $action ) {
			$counter = array( 'count' => 0 );
			return wpagent_companion_live_editor_validate_block(
				array( 'name' => (string) ( $args['block_name'] ?? '' ), 'attributes' => is_array( $args['attributes'] ?? null ) ? $args['attributes'] : array(), 'inner_blocks' => array() ),
				0,
				$counter
			);
		}
		if ( 'update-block' === $action ) {
			$target_name = is_string( $active_content['target_block_name'] ?? null ) ? $active_content['target_block_name'] : '';
			$counter     = array( 'count' => 0 );
			return wpagent_companion_live_editor_validate_block(
				array( 'name' => $target_name, 'attributes' => is_array( $args['attributes'] ?? null ) ? $args['attributes'] : array(), 'inner_blocks' => array() ),
				0,
				$counter
			);
		}
		if ( 'replace-inner-blocks' === $action ) {
			$counter = array( 'count' => 0 );
			foreach ( is_array( $args['blocks'] ?? null ) ? $args['blocks'] : array() as $block ) {
				if ( ! is_array( $block ) ) return new WP_Error( 'wpagent_live_editor_active_blocks', __( 'Ein neuer innerer Block ist ungültig.', 'wp-agent-companion' ), array( 'status' => 400 ) );
				$valid = wpagent_companion_live_editor_validate_block( $block, 0, $counter );
				if ( is_wp_error( $valid ) ) return $valid;
			}
		}
		return true;
	} finally {
		wp_set_current_user( $previous_user_id );
	}
}
