<?php

// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedNamespaceFound -- APD is the established namespace shared with the Pro add-on and retained for backward compatibility.
namespace APD\Support;

use DOMDocument;
use DOMElement;
use DOMNode;

defined('ABSPATH') || exit;

/**
 * Sanitizes static SVG artwork before it reaches the media library or runtime.
 *
 * The designer only needs drawing primitives. Interactive elements, styles,
 * external resources and links are deliberately excluded.
 */
final class SvgSanitizer
{
    public const MAX_BYTES = 2_000_000;

    private const MAX_NODES = 5000;

    private const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';

    private const ALLOWED_ELEMENTS = [
        'svg',
        'g',
        'path',
        'rect',
        'circle',
        'ellipse',
        'line',
        'polyline',
        'polygon',
        'defs',
        'lineargradient',
        'radialgradient',
        'stop',
        'clippath',
        'mask',
        'pattern',
        'text',
        'tspan',
        'title',
        'desc',
    ];

    private const TEXT_ELEMENTS = ['text', 'tspan', 'title', 'desc'];

    private const ALLOWED_ATTRIBUTES = [
        'xmlns',
        'viewbox',
        'preserveaspectratio',
        'x',
        'y',
        'x1',
        'y1',
        'x2',
        'y2',
        'cx',
        'cy',
        'r',
        'rx',
        'ry',
        'width',
        'height',
        'd',
        'points',
        'fill',
        'fill-opacity',
        'fill-rule',
        'stroke',
        'stroke-width',
        'stroke-linecap',
        'stroke-linejoin',
        'stroke-miterlimit',
        'stroke-dasharray',
        'stroke-dashoffset',
        'stroke-opacity',
        'opacity',
        'transform',
        'id',
        'gradientunits',
        'gradienttransform',
        'spreadmethod',
        'offset',
        'stop-color',
        'stop-opacity',
        'clippathunits',
        'maskunits',
        'maskcontentunits',
        'patternunits',
        'patterncontentunits',
        'patterntransform',
        'font-family',
        'font-size',
        'font-weight',
        'font-style',
        'text-anchor',
        'dominant-baseline',
        'letter-spacing',
    ];

    public static function sanitize(string $svg) : string
    {
        $svg = trim($svg);
        if ($svg === '' || strlen($svg) > self::MAX_BYTES || ! class_exists(DOMDocument::class)) {
            return '';
        }

        if (preg_match('/<!DOCTYPE|<!ENTITY/i', $svg)) {
            return '';
        }

        $previousErrors = libxml_use_internal_errors(true);
        libxml_clear_errors();

        try {
            $document = new DOMDocument('1.0', 'UTF-8');
            $loaded = $document->loadXML($svg, LIBXML_NONET | LIBXML_NOBLANKS | LIBXML_NOERROR | LIBXML_NOWARNING);
            $root = $loaded ? $document->documentElement : null;
            if (! $root instanceof DOMElement || strtolower($root->localName) !== 'svg') {
                return '';
            }

            $nodeCount = 0;
            if (! self::sanitizeElement($root, $nodeCount)) {
                return '';
            }

            if (! $root->hasAttribute('xmlns')) {
                $root->setAttribute('xmlns', self::SVG_NAMESPACE);
            }

            $sanitized = $document->saveXML($root);
            return is_string($sanitized) ? trim($sanitized) : '';
        } finally {
            libxml_clear_errors();
            libxml_use_internal_errors($previousErrors);
        }
    }

    private static function sanitizeElement(DOMElement $element, int &$nodeCount) : bool
    {
        $nodeCount++;
        if ($nodeCount > self::MAX_NODES) {
            return false;
        }

        $name = strtolower($element->localName);
        $namespace = (string) $element->namespaceURI;
        if (! in_array($name, self::ALLOWED_ELEMENTS, true) || ($namespace !== '' && $namespace !== self::SVG_NAMESPACE)) {
            return false;
        }

        $attributes = [];
        foreach ($element->attributes as $attribute) {
            $attributes[] = $attribute;
        }

        foreach ($attributes as $attribute) {
            $attributeName = strtolower($attribute->localName ?: $attribute->nodeName);
            $isRootNamespace = $element->parentNode instanceof DOMDocument
                && $attributeName === 'xmlns'
                && $attribute->nodeValue === self::SVG_NAMESPACE;

            if ((! $isRootNamespace && ! in_array($attributeName, self::ALLOWED_ATTRIBUTES, true))
                || ! self::isSafeAttributeValue((string) $attribute->nodeValue)) {
                $element->removeAttributeNode($attribute);
            }
        }

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

        foreach ($children as $child) {
            if ($child instanceof DOMElement) {
                if (! self::sanitizeElement($child, $nodeCount)) {
                    $element->removeChild($child);
                }
                continue;
            }

            if ($child->nodeType === XML_TEXT_NODE) {
                if (! in_array($name, self::TEXT_ELEMENTS, true) && trim((string) $child->nodeValue) !== '') {
                    $element->removeChild($child);
                }
                continue;
            }

            if ($child->nodeType !== XML_CDATA_SECTION_NODE || ! in_array($name, self::TEXT_ELEMENTS, true)) {
                $element->removeChild($child);
            }
        }

        return true;
    }

    private static function isSafeAttributeValue(string $value) : bool
    {
        if (strlen($value) > 250_000 || preg_match('/[\x00-\x08\x0B\x0C\x0E-\x1F]/', $value)) {
            return false;
        }

        if (preg_match('/javascript\s*:|vbscript\s*:|data\s*:|expression\s*\(|@import|-moz-binding|behavior\s*:/i', $value)) {
            return false;
        }

        if (stripos($value, 'url(') !== false) {
            $withoutLocalReferences = preg_replace('/url\(\s*#[A-Za-z_][A-Za-z0-9_.:-]*\s*\)/i', '', $value);
            if (! is_string($withoutLocalReferences) || stripos($withoutLocalReferences, 'url(') !== false) {
                return false;
            }
        }

        return true;
    }
}
