loadXML($raw, LIBXML_NONET | LIBXML_NOERROR | LIBXML_NOWARNING); libxml_clear_errors(); libxml_use_internal_errors($previous); if (!$loaded || !$document->documentElement || strtolower($document->documentElement->localName) !== 'svg') { return false; } // Elements that run code or embed foreign/remote content have no place in a static image. $removeTags = ['script', 'foreignobject', 'handler', 'iframe', 'embed', 'object', 'audio', 'video']; // Animation elements are kept unless they target an event handler or a hyperlink. $animationTags = ['set', 'animate', 'animatetransform', 'animatemotion']; // Strip event handlers, dangerous link targets and url() payloads from one element. $cleanAttributes = function (DOMElement $element): void { foreach (iterator_to_array($element->attributes) as $attribute) { $name = strtolower($attribute->localName); $value = (string)$attribute->value; if (str_starts_with($name, 'on')) { $element->removeAttributeNode($attribute); continue; } if ($name === 'href' && !raijuSvgIsSafeHref($value)) { $element->removeAttributeNode($attribute); continue; } // Paint/style properties can reference javascript: or data: via url(...). if (in_array($name, ['style', 'filter', 'fill', 'stroke', 'mask', 'clip-path'], true) && preg_match('/url\s*\(\s*["\']?\s*(javascript|data)\s*:/i', $value)) { $element->removeAttributeNode($attribute); } } }; $walk = function (DOMNode $node) use (&$walk, $removeTags, $animationTags, $cleanAttributes): void { foreach (iterator_to_array($node->childNodes) as $child) { if ($child instanceof DOMProcessingInstruction) { $node->removeChild($child); continue; } if (!$child instanceof DOMElement) continue; $tag = strtolower($child->localName); if (in_array($tag, $removeTags, true)) { $node->removeChild($child); continue; } // SMIL that animates onload/href can inject script; drop the whole element. if (in_array($tag, $animationTags, true)) { $attributeName = strtolower(trim($child->getAttribute('attributeName'))); if ($attributeName !== '' && (str_starts_with($attributeName, 'on') || $attributeName === 'href' || $attributeName === 'xlink:href')) { $node->removeChild($child); continue; } } $cleanAttributes($child); $walk($child); } }; // The root can carry onload/href itself, so clean it before recursing. $cleanAttributes($document->documentElement); $walk($document->documentElement); $output = $document->saveXML(); if ($output === false) return false; return @file_put_contents($path, $output) !== false; }