/** * Replace constant expressions in given AVT * * @param DOMAttr $attribute * @return void */ protected function replaceAVT(DOMAttr $attribute) { AVTHelper::replace($attribute, function ($token) { if ($token[0] === 'expression') { $token[1] = $this->evaluateExpression($token[1]); } return $token; }); }
/** * Replace an expression with a literal value in given attribute * * @param DOMAttr $attribute * @param string $expr * @param string $value * @return void */ protected function replaceAttribute(DOMAttr $attribute, $expr, $value) { AVTHelper::replace($attribute, function ($token) use($expr, $value) { // Test whether this expression is the one we're looking for if ($token[0] === 'expression' && $token[1] === $expr) { // Replace the expression with the value (as a literal) $token = ['literal', $value]; } return $token; }); }
/** * Convert an attribute value template into PHP * * NOTE: escaping must be performed by the caller * * @link http://www.w3.org/TR/xslt#dt-attribute-value-template * * @param string $attrValue Attribute value template * @return string */ protected function convertAttributeValueTemplate($attrValue) { $phpExpressions = []; foreach (AVTHelper::parse($attrValue) as $token) { if ($token[0] === 'literal') { $phpExpressions[] = var_export($token[1], true); } else { $phpExpressions[] = $this->convertXPath($token[1]); } } return implode('.', $phpExpressions); }
protected function generateAttributes(array $attributes) { if (isset($attributes['style']) && \is_array($attributes['style'])) { $attributes['style'] = $this->generateStyle($attributes['style']); } \ksort($attributes); $xsl = ''; foreach ($attributes as $attrName => $attrValue) { $innerXML = \strpos($attrValue, '<xsl:') !== \false ? $attrValue : AVTHelper::toXSL($attrValue); $xsl .= '<xsl:attribute name="' . \htmlspecialchars($attrName, \ENT_QUOTES, 'UTF-8') . '">' . $innerXML . '</xsl:attribute>'; } return $xsl; }
/** * Normalize the value of an attribute * * @param DOMAttr $attribute * @return void */ protected function normalizeAttribute(DOMAttr $attribute) { // Trim the URL and parse it $tokens = AVTHelper::parse(trim($attribute->value)); $attrValue = ''; foreach ($tokens as list($type, $content)) { if ($type === 'literal') { $attrValue .= BuiltInFilters::sanitizeUrl($content); } else { $attrValue .= '{' . $content . '}'; } } // Unescape brackets in the host part $attrValue = $this->unescapeBrackets($attrValue); // Update the attribute's value $attribute->value = htmlspecialchars($attrValue); }
/** * Remove extraneous space in XPath expressions used in XSL elements * * @param DOMElement $template <xsl:template/> node * @return void */ public function normalize(DOMElement $template) { $xpath = new DOMXPath($template->ownerDocument); // Get all the "match", "select" and "test" attributes of XSL elements, whose value contains // a space $query = '//xsl:*/@*[contains(., " ")][contains("matchselectest", name())]'; foreach ($xpath->query($query) as $attribute) { $attribute->parentNode->setAttribute($attribute->nodeName, XPathHelper::minify($attribute->nodeValue)); } // Get all the attributes of non-XSL elements, whose value contains a space $query = '//*[namespace-uri() != "' . self::XMLNS_XSL . '"]' . '/@*[contains(., " ")]'; foreach ($xpath->query($query) as $attribute) { AVTHelper::replace($attribute, function ($token) { if ($token[0] === 'expression') { $token[1] = XPathHelper::minify($token[1]); } return $token; }); } }
/** * Get all the potential XPath expressions used in given template * * @param DOMElement $template <xsl:template/> node * @return array XPath expression as key, reference node as value */ protected function getExpressions(DOMElement $template) { $xpath = new DOMXPath($template->ownerDocument); $exprs = []; foreach ($xpath->query('//@*') as $attribute) { if ($attribute->parentNode->namespaceURI === self::XMLNS_XSL) { // Attribute of an XSL element. May or may not use XPath, but it shouldn't produce // false-positives $expr = $attribute->value; $exprs[$expr] = $attribute; } else { // Attribute of an HTML (or otherwise) element -- Look for inline expressions foreach (AVTHelper::parse($attribute->value) as $token) { if ($token[0] === 'expression') { $exprs[$token[1]] = $attribute; } } } } return $exprs; }
/** * Replace xsl:value nodes that contain a literal with a Text node * * @param DOMElement $template <xsl:template/> node * @return void */ public function normalize(DOMElement $template) { $xpath = new DOMXPath($template->ownerDocument); foreach ($xpath->query('//xsl:value-of') as $valueOf) { $textContent = $this->getTextContent($valueOf->getAttribute('select')); if ($textContent !== false) { $this->replaceElement($valueOf, $textContent); } } $query = '//*[namespace-uri() != "' . self::XMLNS_XSL . '"]' . '/@*[contains(., "{")]'; foreach ($xpath->query($query) as $attribute) { AVTHelper::replace($attribute, function ($token) { if ($token[0] === 'expression') { $textContent = $this->getTextContent($token[1]); if ($textContent !== false) { // Turn this token into a literal $token = ['literal', $textContent]; } } return $token; }); } }
protected function generateAttributes(array $attributes, $addResponsive = \false) { if ($addResponsive) { $attributes = $this->addResponsiveStyle($attributes); } unset($attributes['responsive']); $xsl = ''; foreach ($attributes as $attrName => $innerXML) { if (\strpos($innerXML, '<') === \false) { $tokens = AVTHelper::parse($innerXML); $innerXML = ''; foreach ($tokens as $_bada9f30) { list($type, $content) = $_bada9f30; if ($type === 'literal') { $innerXML .= \htmlspecialchars($content, \ENT_NOQUOTES, 'UTF-8'); } else { $innerXML .= '<xsl:value-of select="' . \htmlspecialchars($content, \ENT_QUOTES, 'UTF-8') . '"/>'; } } } $xsl .= '<xsl:attribute name="' . \htmlspecialchars($attrName, \ENT_QUOTES, 'UTF-8') . '">' . $innerXML . '</xsl:attribute>'; } return $xsl; }
/** * Append an <output/> element to given node in the IR * * @param DOMElement $ir Parent node * @param string $type Either 'avt', 'literal' or 'xpath' * @param string $content Content to output * @return void */ protected static function appendOutput(DOMElement $ir, $type, $content) { // Reparse AVTs and add them as separate xpath/literal outputs if ($type === 'avt') { foreach (AVTHelper::parse($content) as $token) { $type = $token[0] === 'expression' ? 'xpath' : 'literal'; self::appendOutput($ir, $type, $token[1]); } return; } if ($type === 'xpath') { // Remove whitespace surrounding XPath expressions $content = trim($content); } if ($type === 'literal' && $content === '') { // Don't add empty literals return; } self::appendElement($ir, 'output', htmlspecialchars($content))->setAttribute('type', $type); }
/** * Test whether an attribute node is safe * * @param DOMAttr $attribute Attribute node * @param Tag $tag Reference tag * @return void */ protected function checkAttributeNode(DOMAttr $attribute, Tag $tag) { // Parse the attribute value for XPath expressions and assess their safety foreach (AVTHelper::parse($attribute->value) as $token) { if ($token[0] === 'expression') { $this->checkExpression($attribute, $token[1], $tag); } } }
/** * @testdox toXSL() tests * @dataProvider getToXSLTests */ public function testToXSL($attrValue, $expected) { $this->assertSame($expected, AVTHelper::toXSL($attrValue)); }
/** * Get the regexp used to remove meta elements from the intermediate representation * * @param array $templates * @return string */ public static function getMetaElementsRegexp(array $templates) { $exprs = []; // Coalesce all templates and load them into DOM $xsl = '<xsl:template xmlns:xsl="http://www.w3.org/1999/XSL/Transform">' . implode('', $templates) . '</xsl:template>'; $dom = new DOMDocument(); $dom->loadXML($xsl); $xpath = new DOMXPath($dom); // Collect the values of all the "match", "select" and "test" attributes of XSL elements $query = '//xsl:*/@*[contains("matchselectest", name())]'; foreach ($xpath->query($query) as $attribute) { $exprs[] = $attribute->value; } // Collect the XPath expressions used in all the attributes of non-XSL elements $query = '//*[namespace-uri() != "' . self::XMLNS_XSL . '"]/@*'; foreach ($xpath->query($query) as $attribute) { foreach (AVTHelper::parse($attribute->value) as $token) { if ($token[0] === 'expression') { $exprs[] = $token[1]; } } } // Names of the meta elements $tagNames = ['e' => true, 'i' => true, 's' => true]; // In the highly unlikely event the meta elements are rendered, we remove them from the list foreach (array_keys($tagNames) as $tagName) { if (isset($templates[$tagName]) && $templates[$tagName] !== '') { unset($tagNames[$tagName]); } } // Create a regexp that matches the tag names used as element names, e.g. "s" in "//s" but // not in "@s" or "$s" $regexp = '(\\b(?<![$@])(' . implode('|', array_keys($tagNames)) . ')(?!-)\\b)'; // Now look into all of the expressions that we've collected preg_match_all($regexp, implode("\n", $exprs), $m); foreach ($m[0] as $tagName) { unset($tagNames[$tagName]); } if (empty($tagNames)) { // Always-false regexp return '((?!))'; } return '(<' . RegexpBuilder::fromList(array_keys($tagNames)) . '>[^<]*</[^>]+>)'; }