Skip to content

Commit

Permalink
[TASK] Add NamespaceDetectionTemplateProcessor
Browse files Browse the repository at this point in the history
This commit adds an NamespaceDetectionTemplateProcessor
that takes care of 3 things:

- replace cdata sections with empty lines (including nested cdata)
- register/ignore namespaces through xmlns and shorthand syntax
- report any unregistered/unignored namespaces through exception
  • Loading branch information
Marc Neuhaus committed Dec 28, 2015
1 parent 3ed660d commit 978071f
Show file tree
Hide file tree
Showing 7 changed files with 302 additions and 70 deletions.
1 change: 0 additions & 1 deletion examples/Resources/Private/Singles/Namespaces.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

<fluid xmlns:f="http://typo3.org/ns/TYPO3Fluid/Fluid/ViewHelpers"
xmlns:alias="http://typo3.org/ns/TYPO3Fluid/Fluid/ViewHelpers"
xmlns:phpalias="TYPO3Fluid\\Fluid\\ViewHelpers">
Expand Down
10 changes: 0 additions & 10 deletions src/Core/Parser/Patterns.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,6 @@ abstract class Patterns {
const NAMESPACEPREFIX = 'http://typo3.org/ns/';
const NAMESPACESUFFIX = '/ViewHelpers';

/**
* @var string
*/
static public $SPLIT_PATTERN_TEMPLATE_OPEN_NAMESPACETAG = '/xmlns:([a-z0-9\.]+)=("[^"]+"|\'[^\']+\')*/xi';

/**
* @var string
*/
static public $NAMESPACE_DECLARATION = '/(?<!\\\\){namespace\s*(?P<identifier>[a-zA-Z\*]+[a-zA-Z0-9\.\*]*)\s*(=\s*(?P<phpNamespace>(?:[A-Za-z0-9\.]+|Tx)(?:\\\\\w+)+)\s*)?}/m';

/**
* This regular expression splits the input string at all dynamic tags, AND
* on all <![CDATA[...]]> sections.
Expand Down
58 changes: 4 additions & 54 deletions src/Core/Parser/TemplateParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\RootNode;
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\TextNode;
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\ViewHelperNode;
use TYPO3Fluid\Fluid\Core\Parser\TemplateProcessor\UnknownNamespaceDetectionTemplateProcessor;
use TYPO3Fluid\Fluid\Core\Parser\TemplateProcessor\NamespaceDetectionTemplateProcessor;
use TYPO3Fluid\Fluid\Core\Variables\StandardVariableProvider;
use TYPO3Fluid\Fluid\Core\Variables\VariableExtractor;
use TYPO3Fluid\Fluid\Core\Variables\VariableProviderInterface;
Expand Down Expand Up @@ -88,6 +88,7 @@ public function __construct(ViewHelperResolver $viewHelperResolver = NULL) {
}
$this->viewHelperResolver = $viewHelperResolver;
$this->variableProvider = new StandardVariableProvider();
$this->templateProcessors[] = new NamespaceDetectionTemplateProcessor();
}

/**
Expand Down Expand Up @@ -155,7 +156,6 @@ public function parse($templateString, $templateIdentifier = NULL) {
try {
$this->reset();
$templateString = $this->preProcessTemplateSource($templateString);
$this->registerNamespacesFromTemplateSource($templateString);

$splitTemplate = $this->splitTemplateAtDynamicTags($templateString);
$parsingState = $this->buildObjectTree($splitTemplate, self::CONTEXT_OUTSIDE_VIEWHELPER_ARGUMENTS);
Expand Down Expand Up @@ -204,43 +204,6 @@ protected function preProcessTemplateSource($templateSource) {
return $templateSource;
}

/**
* Pre-process the template source before it is returned to the
* TemplateParser or passed to the next TemplateProcessorInterface instance.
*
* @param string $templateSource
* @return string
*/
protected function registerNamespacesFromTemplateSource($templateSource) {
preg_match_all(Patterns::$NAMESPACE_DECLARATION, $templateSource, $namespaces);
foreach ($namespaces['identifier'] as $key => $identifier) {
$namespace = $namespaces['phpNamespace'][$key];
if (strlen($namespace) === 0) {
$namespace = NULL;
}
$this->viewHelperResolver->registerNamespace($identifier, $namespace);
}

preg_match_all(Patterns::$SPLIT_PATTERN_TEMPLATE_DYNAMICTAGS, $templateSource, $splitParts);
if (isset($splitParts[0])) {
foreach ($splitParts[0] as $viewHelper) {
preg_match_all(Patterns::$SCAN_PATTERN_TEMPLATE_VIEWHELPERTAG, $viewHelper, $matches);
foreach ($matches['NamespaceIdentifier'] as $key => $namespace) {
if (!$this->viewHelperResolver->isNamespaceValidOrIgnored($namespace)) {
throw new UnknownNamespaceException('Unkown Namespace: ' . htmlspecialchars($matches[0][$key]));
}
}
}
}

preg_match_all(Patterns::$SPLIT_PATTERN_SHORTHANDSYNTAX_VIEWHELPER, $templateSource, $shorthandViewHelpers);
foreach ($shorthandViewHelpers['NamespaceIdentifier'] as $key => $namespace) {
if (!$this->viewHelperResolver->isNamespaceValidOrIgnored($namespace)) {
throw new UnknownNamespaceException('Unkown Namespace: ' . $shorthandViewHelpers[0][$key]);
}
}
}

/**
* Resets the parser to its default values.
*
Expand Down Expand Up @@ -285,19 +248,8 @@ protected function buildObjectTree(array $splitTemplate, $context) {
$this->pointerLineCharacter = strlen(substr($previousBlock, strrpos($previousBlock, PHP_EOL))) + 1;
$previousBlock = $templateElement;
$matchedVariables = array();
if (preg_match_all(Patterns::$SPLIT_PATTERN_TEMPLATE_OPEN_NAMESPACETAG, $templateElement, $matchedVariables, PREG_SET_ORDER) > 0) {
foreach ($matchedVariables as $namespaceMatch) {
$viewHelperNamespace = $this->unquoteString($namespaceMatch[2]);
$phpNamespace = $this->viewHelperResolver->resolvePhpNamespaceFromFluidNamespace($viewHelperNamespace);
$this->viewHelperResolver->registerNamespace($namespaceMatch[1], $phpNamespace);
}
continue;
} elseif (trim($templateElement) === '</f:fluid>' || trim($templateElement) === '</fluid>') {
continue;
} elseif (preg_match(Patterns::$SCAN_PATTERN_CDATA, $templateElement, $matchedVariables) > 0) {
$this->textHandler($state, $matchedVariables[1]);
continue;
} elseif (preg_match(Patterns::$SCAN_PATTERN_TEMPLATE_VIEWHELPERTAG, $templateElement, $matchedVariables) > 0) {

if (preg_match(Patterns::$SCAN_PATTERN_TEMPLATE_VIEWHELPERTAG, $templateElement, $matchedVariables) > 0) {
if ($this->openingViewHelperTagHandler(
$state,
$matchedVariables['NamespaceIdentifier'],
Expand All @@ -324,8 +276,6 @@ protected function buildObjectTree(array $splitTemplate, $context) {
throw new Exception('Not all tags were closed!', 1238169398);
}
return $state;


}
/**
* Handles an opening or self-closing view helper tag.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
<?php
namespace TYPO3Fluid\Fluid\Core\Parser\TemplateProcessor;

/*
* This file belongs to the package "TYPO3 Fluid".
* See LICENSE.txt that was shipped with this package.
*/

use TYPO3Fluid\Fluid\Core\Parser\Patterns;
use TYPO3Fluid\Fluid\Core\Parser\TemplateParser;
use TYPO3Fluid\Fluid\Core\Parser\UnknownNamespaceException;
use TYPO3Fluid\Fluid\Core\Parser\TemplateProcessorInterface;
use TYPO3Fluid\Fluid\Core\ViewHelper\ViewHelperResolver;

/**
* This template processor takes care of the following things:
*
* - replace cdata sections with empty lines (including nested cdata)
* - register/ignore namespaces through xmlns and shorthand syntax
* - report any unregistered/unignored namespaces through exception
*
*/
class NamespaceDetectionTemplateProcessor implements TemplateProcessorInterface {

const NAMESPACE_DECLARATION = '/(?<!\\\\){namespace\s*(?P<identifier>[a-zA-Z\*]+[a-zA-Z0-9\.\*]*)\s*(=\s*(?P<phpNamespace>(?:[A-Za-z0-9\.]+|Tx)(?:\\\\\w+)+)\s*)?}/m';

const SPLIT_PATTERN_TEMPLATE_OPEN_NAMESPACETAG = '/xmlns:([a-z0-9\.]+)=("[^"]+"|\'[^\']+\')*/xi';

/**
* @var TemplateParser
*/
protected $templateParser;

/**
* @var ViewHelperResolver
*/
protected $viewHelperResolver;

/**
* @var array()
*/
protected $localNamespaces = array();

/**
* Setter for passing the TemplateParser instance
* that is currently processing the template.
*
* @param TemplateParser $templateParser
* @return void
*/
public function setTemplateParser(TemplateParser $templateParser) {
$this->templateParser = $templateParser;
}

/**
* Setter for passing the ViewHelperResolver instance
* being used by the TemplateParser to resolve classes
* and namespaces of ViewHelpers.
*
* @param ViewHelperResolver $viewHelperResolver
* @return void
*/
public function setViewHelperResolver(ViewHelperResolver $viewHelperResolver) {
$this->viewHelperResolver = $viewHelperResolver;
}

/**
* Pre-process the template source before it is
* returned to the TemplateParser or passed to
* the next TemplateProcessorInterface instance.
*
* @param string $templateSource
* @return string
*/
public function preProcessSource($templateSource) {
$templateSource = $this->replaceCdataSectionsByEmptyLines($templateSource);
$this->registerNamespacesFromTemplateSource($templateSource);
$this->throwExceptionsForUnhandledNamespaces($templateSource);
return $templateSource;
}

/**
* Replaces all cdata sections with empty lines to exclude it from further
* processing in the templateParser while maintaining the line-count
* of the template string for the exception handler to reference to.
*
* @param string $templateSource
* @return string
*/
public function replaceCdataSectionsByEmptyLines($templateSource) {
$parts = preg_split('/(\<\!\[CDATA\[|\]\]\>)/', $templateSource, -1, PREG_SPLIT_DELIM_CAPTURE);

$balance = 0;
foreach ($parts as $index => $part) {
if ($part === '<![CDATA[') {
$balance++;
}
if ($balance > 0) {
$parts[$index] = str_repeat(PHP_EOL, substr_count($part, PHP_EOL));
}
if ($part === ']]>') {
$balance--;
}
}

return implode('', $parts);
}

/**
* Register all namespaces that are declared inside the template string
*
* @param string $templateSource
* @return void
*/
public function registerNamespacesFromTemplateSource($templateSource) {
if (preg_match_all(static::SPLIT_PATTERN_TEMPLATE_OPEN_NAMESPACETAG, $templateSource, $matchedVariables, PREG_SET_ORDER) > 0) {
foreach ($matchedVariables as $namespaceMatch) {
$viewHelperNamespace = $this->unquoteString($namespaceMatch[2]);
$phpNamespace = $this->viewHelperResolver->resolvePhpNamespaceFromFluidNamespace($viewHelperNamespace);
if (stristr($phpNamespace, '/') === FALSE) {
$this->viewHelperResolver->registerNamespace($namespaceMatch[1], $phpNamespace);
}
}
}

preg_match_all(static::NAMESPACE_DECLARATION, $templateSource, $namespaces);
foreach ($namespaces['identifier'] as $key => $identifier) {
$namespace = $namespaces['phpNamespace'][$key];
if (strlen($namespace) === 0) {
$namespace = NULL;
}
$this->viewHelperResolver->registerNamespace($identifier, $namespace);
}
}

/**
* Throw an UnknownNamespaceException for any unknown and not ignored
* namespace inside the template string
*
* @param string $templateSource
* @return void
*/
public function throwExceptionsForUnhandledNamespaces($templateSource) {
$splitTemplate = preg_split(Patterns::$SPLIT_PATTERN_TEMPLATE_DYNAMICTAGS, $templateSource, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
foreach ($splitTemplate as $templateElement) {
if (preg_match(Patterns::$SCAN_PATTERN_TEMPLATE_VIEWHELPERTAG, $templateElement, $matchedVariables) > 0) {
if (!$this->viewHelperResolver->isNamespaceValidOrIgnored($matchedVariables['NamespaceIdentifier'])) {
throw new UnknownNamespaceException('Unkown Namespace: ' . htmlspecialchars($matchedVariables[0]));
}
continue;
} elseif (preg_match(Patterns::$SCAN_PATTERN_TEMPLATE_CLOSINGVIEWHELPERTAG, $templateElement, $matchedVariables) > 0) {
continue;
}

$sections = preg_split(Patterns::$SPLIT_PATTERN_SHORTHANDSYNTAX, $templateElement, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
foreach ($sections as $section) {
if (preg_match(Patterns::$SCAN_PATTERN_SHORTHANDSYNTAX_OBJECTACCESSORS, $section, $matchedVariables) > 0) {
preg_match_all(Patterns::$SPLIT_PATTERN_SHORTHANDSYNTAX_VIEWHELPER, $section, $shorthandViewHelpers, PREG_SET_ORDER);
foreach ($shorthandViewHelpers as $shorthandViewHelper) {
if (!$this->viewHelperResolver->isNamespaceValidOrIgnored($shorthandViewHelper['NamespaceIdentifier'])) {
throw new UnknownNamespaceException('Unkown Namespace: ' . $shorthandViewHelper['NamespaceIdentifier']);
}
}
}
}
}
}

/**
* Removes escapings from a given argument string and trims the outermost
* quotes.
*
* This method is meant as a helper for regular expression results.
*
* @param string $quotedValue Value to unquote
* @return string Unquoted value
*/
protected function unquoteString($quotedValue) {
$value = $quotedValue;
if ($quotedValue{0} === '"') {
$value = str_replace('\\"', '"', preg_replace('/(^"|"$)/', '', $quotedValue));
} elseif ($quotedValue{0} === '\'') {
$value = str_replace("\\'", "'", preg_replace('/(^\'|\'$)/', '', $quotedValue));
}
return str_replace('\\\\', '\\', $value);
}
}
7 changes: 3 additions & 4 deletions tests/Functional/ExamplesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -156,9 +156,8 @@ public function getExampleScriptTestValues() {
array(
'NamespaceResolving template from Singles.',
'Argument passed to CustomViewHelper:',
'\'123\''
),
'\TYPO3Fluid\Fluid\Core\Parser\UnknownNamespaceException'
'123'
)
),
'example_single.php' => array(
'example_single.php',
Expand Down Expand Up @@ -208,4 +207,4 @@ public function getExampleScriptTestValues() {
);
}

}
}
13 changes: 13 additions & 0 deletions tests/Unit/Core/Parser/TemplateParserPatternTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,26 @@
*/

use TYPO3Fluid\Fluid\Core\Parser\Patterns;
use TYPO3Fluid\Fluid\Core\Parser\TemplateProcessor\NamespaceDetectionTemplateProcessor;
use TYPO3Fluid\Fluid\Tests\UnitTestCase;

/**
* Testcase for Regular expressions in parser
*/
class PatternsTest extends UnitTestCase {

/**
* @test
*/
public function testSCAN_PATTERN_NAMESPACEDECLARATION() {
$this->assertEquals(preg_match(NamespaceDetectionTemplateProcessor::NAMESPACE_DECLARATION, '{namespace acme=Acme.MyPackage\Bla\blubb}'), 1, 'The SCAN_PATTERN_NAMESPACEDECLARATION pattern did not match a namespace declaration (1).');
$this->assertEquals(preg_match(NamespaceDetectionTemplateProcessor::NAMESPACE_DECLARATION, '{namespace acme=Acme.MyPackage\Bla\Blubb }'), 1, 'The SCAN_PATTERN_NAMESPACEDECLARATION pattern did not match a namespace declaration (2).');
$this->assertEquals(preg_match(NamespaceDetectionTemplateProcessor::NAMESPACE_DECLARATION, ' {namespace foo = Foo\Bla3\Blubb } '), 1, 'The SCAN_PATTERN_NAMESPACEDECLARATION pattern did not match a namespace declaration (3).');
$this->assertEquals(preg_match(NamespaceDetectionTemplateProcessor::NAMESPACE_DECLARATION, ' {namespace foo.bar = Foo\Bla3\Blubb } '), 1, 'The SCAN_PATTERN_NAMESPACEDECLARATION pattern did not match a namespace declaration (4).');
$this->assertEquals(preg_match(NamespaceDetectionTemplateProcessor::NAMESPACE_DECLARATION, ' \{namespace fblubb = TYPO3.Fluid\Bla3\Blubb }'), 0, 'The SCAN_PATTERN_NAMESPACEDECLARATION pattern did match a namespace declaration even if it was escaped. (1)');
$this->assertEquals(preg_match(NamespaceDetectionTemplateProcessor::NAMESPACE_DECLARATION, '\{namespace typo3 = TYPO3.TYPO3\Bla3\Blubb }'), 0, 'The SCAN_PATTERN_NAMESPACEDECLARATION pattern did match a namespace declaration even if it was escaped. (2)');
}

/**
* @test
*/
Expand Down
Loading

0 comments on commit 978071f

Please sign in to comment.