ownerDocument->saveXML($element); $pos = 1 + strpos($xml, '>'); $len = strrpos($xml, '<') - $pos; // If the template is empty, return an empty string return ($len < 1) ? '' : substr($xml, $pos, $len); } /** * Load a template as an xsl:template node * * Will attempt to load it as XML first, then as HTML as a fallback. Either way, an xsl:template * node is returned * * @param string $template * @return DOMDocument */ public static function load($template) { $dom = self::loadAsXML($template) ?: self::loadAsXML(self::fixEntities($template)); if ($dom) { return $dom; } // If the template contains an XSL element, abort now. Otherwise, try reparsing it as HTML if (strpos($template, 'message); } return self::loadAsHTML($template); } /** * Serialize a loaded template back into a string * * NOTE: removes the root node created by load() * * @param DOMDocument $dom * @return string */ public static function save(DOMDocument $dom) { $xml = self::innerXML($dom->documentElement); if (strpos($xml, 'xmlns:xsl') !== false) { $xml = preg_replace('((<[^>]+?) xmlns:xsl="' . self::XMLNS_XSL . '")', '$1', $xml); } return $xml; } /** * Replace HTML entities and unescaped ampersands in given template * * @param string $template * @return string */ protected static function fixEntities($template) { $template = self::replaceEntities($template); $template = preg_replace('(&(?!quot;|amp;|apos;|lt;|gt;|#\\d+;|#x[A-Fa-f0-9]+;))', '&', $template); return $template; } /** * Load given HTML template in a DOM document * * @param string $template Original template * @return DOMDocument */ protected static function loadAsHTML($template) { $template = self::replaceCDATA($template); $template = self::replaceEntities($template); $dom = new DOMDocument; $html = '
' . $template . '
'; $useErrors = libxml_use_internal_errors(true); $dom->loadHTML($html, LIBXML_NSCLEAN); self::removeInvalidAttributes($dom); libxml_use_internal_errors($useErrors); // Now dump the thing as XML then reload it with the proper root element $xml = '' . self::innerXML($dom->documentElement->firstChild->firstChild) . ''; $useErrors = libxml_use_internal_errors(true); $dom->loadXML($xml, LIBXML_NSCLEAN); libxml_use_internal_errors($useErrors); return $dom; } /** * Load given XSL template in a DOM document * * @param string $template Original template * @return bool|DOMDocument DOMDocument on success, FALSE otherwise */ protected static function loadAsXML($template) { $xml = '' . $template . ''; $useErrors = libxml_use_internal_errors(true); $dom = new DOMDocument; $success = $dom->loadXML($xml, LIBXML_NOCDATA | LIBXML_NSCLEAN); self::removeInvalidAttributes($dom); libxml_use_internal_errors($useErrors); return ($success) ? $dom : false; } /** * Remove attributes with an invalid name from given DOM document * * @param DOMDocument $dom * @return void */ protected static function removeInvalidAttributes(DOMDocument $dom) { $xpath = new DOMXPath($dom); foreach ($xpath->query('//@*') as $attribute) { if (!preg_match('(^(?:[-\\w]+:)?(?!\\d)[-\\w]+$)D', $attribute->nodeName)) { $attribute->parentNode->removeAttributeNode($attribute); } } } /** * Replace CDATA sections in given template * * @param string $template Original template * @return string Modified template */ protected static function replaceCDATA($template) { return preg_replace_callback( '()', function ($m) { return htmlspecialchars($m[1]); }, $template ); } /** * Replace known HTML entities * * @param string $template * @return string */ protected static function replaceEntities(string $template): string { return preg_replace_callback( '(&(?!quot;|amp;|apos;|lt;|gt;)\\w+;)', function ($m) { return html_entity_decode($m[0], ENT_HTML5 | ENT_NOQUOTES, 'UTF-8'); }, str_replace('&', '&', $template) ); } }