<?php
/**
 * Shared SVG upload and render helpers.
 *
 * @package AxcelersBlocks
 */

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

/**
 * Whether the current user may upload SVG files.
 *
 * @return bool
 */
function axcelersblocks_current_user_can_upload_svg() {
	return current_user_can( 'manage_options' );
}

/**
 * Allow SVG uploads while the plugin is active.
 *
 * @param array $mimes Allowed mime types.
 * @return array
 */
function axcelersblocks_allow_svg_uploads( $mimes ) {
	if ( ! axcelersblocks_current_user_can_upload_svg() ) {
		return $mimes;
	}

	$mimes['svg'] = 'image/svg+xml';

	return $mimes;
}
add_filter( 'upload_mimes', 'axcelersblocks_allow_svg_uploads' );

/**
 * Ensure WordPress recognises SVG uploads consistently.
 *
 * @param array        $data     File data array.
 * @param string       $file     Full path to the file.
 * @param string       $filename File name.
 * @param array<string,string> $mimes Allowed mime types.
 * @param string|false $real_mime Real mime type.
 * @return array
 */
function axcelersblocks_fix_svg_upload_filetype( $data, $file, $filename, $mimes, $real_mime ) {
	if ( ! axcelersblocks_current_user_can_upload_svg() ) {
		return $data;
	}

	$extension = strtolower( pathinfo( $filename, PATHINFO_EXTENSION ) );

	if ( 'svg' === $extension ) {
		$data['ext']  = 'svg';
		$data['type'] = 'image/svg+xml';
	}

	return $data;
}
add_filter( 'wp_check_filetype_and_ext', 'axcelersblocks_fix_svg_upload_filetype', 10, 5 );

/**
 * Whether a CSS string contains obviously unsafe content for inline SVG.
 *
 * @param string $css CSS string.
 * @return bool
 */
function axcelersblocks_svg_has_dangerous_css( $css ) {
	if ( ! is_string( $css ) ) {
		return false;
	}

	return (bool) preg_match( '/(?:expression\s*\(|javascript\s*:|@import|behavior\s*:|url\s*\(\s*["\']?\s*javascript\s*:)/i', $css );
}

/**
 * Allowed SVG element names for upload sanitisation.
 *
 * @return string[]
 */
function axcelersblocks_get_allowed_svg_elements() {
	return [
		'svg',
		'g',
		'path',
		'rect',
		'circle',
		'ellipse',
		'line',
		'polyline',
		'polygon',
		'defs',
		'clippath',
		'mask',
		'pattern',
		'symbol',
		'use',
		'image',
		'lineargradient',
		'radialgradient',
		'stop',
		'title',
		'desc',
		'style',
	];
}

/**
 * Allowed SVG attribute names for upload sanitisation.
 *
 * @return string[]
 */
function axcelersblocks_get_allowed_svg_attributes() {
	return [
		'xmlns',
		'xmlns:xlink',
		'viewbox',
		'width',
		'height',
		'x',
		'y',
		'x1',
		'y1',
		'x2',
		'y2',
		'cx',
		'cy',
		'r',
		'rx',
		'ry',
		'fx',
		'fy',
		'd',
		'points',
		'fill',
		'fill-opacity',
		'fill-rule',
		'stroke',
		'stroke-width',
		'stroke-linecap',
		'stroke-linejoin',
		'stroke-dasharray',
		'stroke-dashoffset',
		'stroke-opacity',
		'stroke-miterlimit',
		'opacity',
		'transform',
		'class',
		'id',
		'style',
		'preserveaspectratio',
		'focusable',
		'role',
		'aria-hidden',
		'aria-label',
		'offset',
		'stop-color',
		'stop-opacity',
		'gradientunits',
		'gradienttransform',
		'spreadmethod',
		'patternunits',
		'patterncontentunits',
		'patterntransform',
		'maskunits',
		'maskcontentunits',
		'clippathunits',
		'href',
		'xlink:href',
		'xml:space',
		'type',
		'data-name',
	];
}

/**
 * Sanitize raw SVG markup for safe inline rendering.
 *
 * @param string $svg_markup Raw SVG markup.
 * @return string
 */
function axcelersblocks_sanitize_svg_markup( $svg_markup ) {
	if ( ! is_string( $svg_markup ) ) {
		return '';
	}

	$svg_markup = trim( $svg_markup );

	if ( '' === $svg_markup || false === stripos( $svg_markup, '<svg' ) || ! class_exists( 'DOMDocument' ) ) {
		return '';
	}

	$previous_use_errors = libxml_use_internal_errors( true );
	$dom                 = new DOMDocument();
	$loaded              = $dom->loadXML( $svg_markup, LIBXML_NONET | LIBXML_NOERROR | LIBXML_NOWARNING | LIBXML_COMPACT );

	libxml_clear_errors();
	libxml_use_internal_errors( $previous_use_errors );

	if ( ! $loaded || ! $dom->documentElement || 'svg' !== strtolower( $dom->documentElement->nodeName ) ) {
		return '';
	}

	$allowed_elements = axcelersblocks_get_allowed_svg_elements();
	$allowed_attrs    = axcelersblocks_get_allowed_svg_attributes();

	$walker = static function ( DOMNode $node ) use ( &$walker, $allowed_elements, $allowed_attrs ) {
		if ( XML_COMMENT_NODE === $node->nodeType || XML_PI_NODE === $node->nodeType ) {
			if ( $node->parentNode ) {
				$node->parentNode->removeChild( $node );
			}
			return;
		}

		if ( XML_TEXT_NODE === $node->nodeType ) {
			$parent_name = $node->parentNode instanceof DOMElement ? strtolower( $node->parentNode->nodeName ) : '';
			if ( 'style' === $parent_name && axcelersblocks_svg_has_dangerous_css( $node->nodeValue ?? '' ) ) {
				$node->nodeValue = '';
			}
			return;
		}

		if ( XML_ELEMENT_NODE !== $node->nodeType ) {
			return;
		}

		$tag_name = strtolower( $node->nodeName );

		if ( ! in_array( $tag_name, $allowed_elements, true ) ) {
			if ( $node->parentNode ) {
				$node->parentNode->removeChild( $node );
			}
			return;
		}

		if ( 'style' === $tag_name && axcelersblocks_svg_has_dangerous_css( $node->textContent ?? '' ) ) {
			if ( $node->parentNode ) {
				$node->parentNode->removeChild( $node );
			}
			return;
		}

		if ( $node->hasAttributes() ) {
			$attrs = [];
			foreach ( $node->attributes as $attr ) {
				$attrs[] = $attr;
			}

			foreach ( $attrs as $attr ) {
				$attr_name  = $attr->nodeName;
				$attr_lower = strtolower( $attr_name );
				$attr_value = trim( $attr->nodeValue );

				if ( 0 === strpos( $attr_lower, 'on' ) || ! in_array( $attr_lower, $allowed_attrs, true ) ) {
					$node->removeAttribute( $attr_name );
					continue;
				}

				if ( in_array( $attr_lower, [ 'href', 'xlink:href' ], true ) && preg_match( '/^\s*javascript\s*:/i', $attr_value ) ) {
					$node->removeAttribute( $attr_name );
					continue;
				}

				if ( 'style' === $attr_lower && axcelersblocks_svg_has_dangerous_css( $attr_value ) ) {
					$node->removeAttribute( $attr_name );
				}
			}
		}

		$children = [];
		foreach ( $node->childNodes as $child ) {
			$children[] = $child;
		}

		foreach ( $children as $child ) {
			$walker( $child );
		}
	};

	$walker( $dom->documentElement );

	$clean_svg = $dom->saveXML( $dom->documentElement );

	return is_string( $clean_svg ) ? trim( $clean_svg ) : '';
}

/**
 * Sanitize uploaded SVG files before WordPress stores them.
 *
 * @param array $file Uploaded file data.
 * @return array
 */
function axcelersblocks_sanitize_svg_upload( $file ) {
	if ( ! axcelersblocks_current_user_can_upload_svg() ) {
		return $file;
	}

	if ( empty( $file['name'] ) || empty( $file['tmp_name'] ) ) {
		return $file;
	}

	$extension = strtolower( pathinfo( $file['name'], PATHINFO_EXTENSION ) );

	if ( 'svg' !== $extension ) {
		return $file;
	}

	$raw_svg = @file_get_contents( $file['tmp_name'] );

	if ( false === $raw_svg ) {
		$file['error'] = __( 'The SVG file could not be read.', 'axcelersblocks' );
		return $file;
	}

	$clean_svg = axcelersblocks_sanitize_svg_markup( $raw_svg );

	if ( '' === $clean_svg ) {
		$file['error'] = __( 'Please upload a valid SVG file.', 'axcelersblocks' );
		return $file;
	}

	if ( false === @file_put_contents( $file['tmp_name'], $clean_svg ) ) {
		$file['error'] = __( 'The SVG file could not be sanitized.', 'axcelersblocks' );
	}

	return $file;
}
add_filter( 'wp_handle_upload_prefilter', 'axcelersblocks_sanitize_svg_upload' );
add_filter( 'wp_handle_sideload_prefilter', 'axcelersblocks_sanitize_svg_upload' );

/**
 * Whether a URL looks like an SVG asset.
 *
 * @param string $url URL to inspect.
 * @return bool
 */
function axcelersblocks_is_svg_url( $url ) {
	if ( ! is_string( $url ) || '' === $url ) {
		return false;
	}

	$path = wp_parse_url( $url, PHP_URL_PATH );

	if ( ! is_string( $path ) || '' === $path ) {
		return false;
	}

	return 'svg' === strtolower( pathinfo( $path, PATHINFO_EXTENSION ) );
}

/**
 * Get sanitized inline SVG markup from an attachment.
 *
 * @param int $attachment_id Attachment ID.
 * @return string
 */
function axcelersblocks_get_inline_svg_markup_from_attachment( $attachment_id ) {
	$attachment_id = (int) $attachment_id;

	if ( $attachment_id <= 0 || 'image/svg+xml' !== get_post_mime_type( $attachment_id ) ) {
		return '';
	}

	$file = get_attached_file( $attachment_id );

	if ( ! $file || ! file_exists( $file ) ) {
		return '';
	}

	$raw_svg = @file_get_contents( $file );

	if ( false === $raw_svg ) {
		return '';
	}

	return axcelersblocks_sanitize_svg_markup( $raw_svg );
}

/**
 * Get sanitized inline SVG markup from a local attachment URL.
 *
 * @param string $url Attachment URL.
 * @return string
 */
function axcelersblocks_get_inline_svg_markup_from_url( $url ) {
	if ( ! axcelersblocks_is_svg_url( $url ) ) {
		return '';
	}

	$attachment_id = attachment_url_to_postid( $url );

	if ( $attachment_id > 0 ) {
		return axcelersblocks_get_inline_svg_markup_from_attachment( $attachment_id );
	}

	return '';
}
