|
13 | 13 | use PHPUnit\Framework\Attributes\Test; |
14 | 14 | use TYPO3Fluid\Fluid\Core\Compiler\StopCompilingException; |
15 | 15 | use TYPO3Fluid\Fluid\Core\Compiler\TemplateCompiler; |
| 16 | +use TYPO3Fluid\Fluid\Core\ErrorHandler\ErrorHandlerInterface; |
| 17 | +use TYPO3Fluid\Fluid\Core\ErrorHandler\TolerantErrorHandler; |
16 | 18 | use TYPO3Fluid\Fluid\Core\Parser\Exception as ParserException; |
17 | 19 | use TYPO3Fluid\Fluid\Core\Parser\InterceptorInterface; |
18 | 20 | use TYPO3Fluid\Fluid\Core\Parser\ParsingState; |
|
27 | 29 | use TYPO3Fluid\Fluid\Core\Parser\TemplateProcessorInterface; |
28 | 30 | use TYPO3Fluid\Fluid\Core\Parser\UnknownNamespaceException; |
29 | 31 | use TYPO3Fluid\Fluid\Core\Rendering\RenderingContext; |
| 32 | +use TYPO3Fluid\Fluid\Core\ViewHelper\Exception as ViewHelperException; |
30 | 33 | use TYPO3Fluid\Fluid\Tests\Functional\AbstractFunctionalTestCase; |
31 | 34 | use TYPO3Fluid\Fluid\Tests\Functional\Fixtures\Various\ParserConfigurationAccessRenderingContext; |
32 | 35 |
|
@@ -101,13 +104,92 @@ public function invalidViewHelperCallThrowsException(string $source, int $except |
101 | 104 | $subject->parse($source); |
102 | 105 | } |
103 | 106 |
|
| 107 | + #[Test] |
| 108 | + public function parseExceptionUsesOriginalTemplatePathForContext(): void |
| 109 | + { |
| 110 | + $renderingContext = new RenderingContext(); |
| 111 | + $subject = $renderingContext->getTemplateParser(); |
| 112 | + |
| 113 | + try { |
| 114 | + $subject->parse('<f:render>', 'identifier-name', '/path/to/original/template.html'); |
| 115 | + self::fail('Expected parse() to throw for invalid template source.'); |
| 116 | + } catch (ParserException $exception) { |
| 117 | + self::assertStringContainsString( |
| 118 | + 'Fluid parse error in template /path/to/original/template.html', |
| 119 | + $exception->getMessage(), |
| 120 | + ); |
| 121 | + self::assertStringNotContainsString( |
| 122 | + 'Fluid parse error in template identifier-name', |
| 123 | + $exception->getMessage(), |
| 124 | + ); |
| 125 | + self::assertSame('/path/to/original/template.html', $exception->getTemplateLocation()->identifierOrPath); |
| 126 | + } |
| 127 | + } |
| 128 | + |
| 129 | + #[Test] |
| 130 | + public function parseExceptionStartsAtFirstLineAndCharacterAfterReset(): void |
| 131 | + { |
| 132 | + $renderingContext = new RenderingContext(); |
| 133 | + $subject = $renderingContext->getTemplateParser(); |
| 134 | + |
| 135 | + try { |
| 136 | + $subject->parse('<f:render>'); |
| 137 | + self::fail('Expected parse() to throw for invalid template source.'); |
| 138 | + } catch (ParserException $exception) { |
| 139 | + self::assertStringContainsString('line 1 at character 1', $exception->getMessage()); |
| 140 | + self::assertSame(1, $exception->getTemplateLocation()->line); |
| 141 | + self::assertSame(1, $exception->getTemplateLocation()->character); |
| 142 | + } |
| 143 | + } |
| 144 | + |
| 145 | + #[Test] |
| 146 | + public function parseExceptionInViewHelperArgumentUsesOuterTemplateChunk(): void |
| 147 | + { |
| 148 | + $renderingContext = new RenderingContext(); |
| 149 | + $renderingContext->getViewHelperResolver()->addNamespace('test', 'TYPO3Fluid\Fluid\Tests\Functional\Fixtures\ViewHelpers'); |
| 150 | + $subject = $renderingContext->getTemplateParser(); |
| 151 | + |
| 152 | + try { |
| 153 | + $subject->parse('<test:requiredArgument required="{invalid:foo()}" />'); |
| 154 | + self::fail('Expected parse() to throw for invalid template source.'); |
| 155 | + } catch (ParserException $exception) { |
| 156 | + self::assertStringContainsString( |
| 157 | + 'Template source chunk: <test:requiredArgument required="{invalid:foo()}" />', |
| 158 | + $exception->getMessage(), |
| 159 | + ); |
| 160 | + self::assertStringNotContainsString( |
| 161 | + 'Template source chunk: {invalid:foo()}', |
| 162 | + $exception->getMessage(), |
| 163 | + ); |
| 164 | + } |
| 165 | + } |
| 166 | + |
| 167 | + #[Test] |
| 168 | + public function parseExceptionAfterMultilinePreviousBlockUsesLastLineCharacterOffset(): void |
| 169 | + { |
| 170 | + $renderingContext = new RenderingContext(); |
| 171 | + $subject = $renderingContext->getTemplateParser(); |
| 172 | + |
| 173 | + try { |
| 174 | + $subject->parse("long-prefix\nx\n<f:render>"); |
| 175 | + self::fail('Expected parse() to throw for invalid template source.'); |
| 176 | + } catch (ParserException $exception) { |
| 177 | + self::assertStringContainsString('line 3 at character 2', $exception->getMessage()); |
| 178 | + self::assertStringNotContainsString('line 3 at character 14', $exception->getMessage()); |
| 179 | + self::assertSame(3, $exception->getTemplateLocation()->line); |
| 180 | + self::assertSame(2, $exception->getTemplateLocation()->character); |
| 181 | + } |
| 182 | + } |
| 183 | + |
104 | 184 | #[Test] |
105 | 185 | public function providedRequiredViewHelperArgumentThrowsNoException(): void |
106 | 186 | { |
107 | 187 | $renderingContext = new RenderingContext(); |
108 | 188 | $renderingContext->getViewHelperResolver()->addNamespace('test', 'TYPO3Fluid\Fluid\Tests\Functional\Fixtures\ViewHelpers'); |
109 | 189 | $subject = $renderingContext->getTemplateParser(); |
110 | | - self::assertInstanceOf(ParsingState::class, $subject->parse('<test:requiredArgument required="test" />')); |
| 190 | + $parsedTemplate = $subject->parse('<test:requiredArgument required="test" />', 'identifier-name'); |
| 191 | + self::assertInstanceOf(ParsingState::class, $parsedTemplate); |
| 192 | + self::assertSame('identifier-name', $parsedTemplate->getIdentifier()); |
111 | 193 | } |
112 | 194 |
|
113 | 195 | public static function validateAdditionalArgumentsGetsCalledWithUndefinedArgumentsDataProvider(): array |
@@ -139,6 +221,62 @@ public function validateAdditionalArgumentsGetsCalledWithUndefinedArguments(stri |
139 | 221 | $subject->parse($source); |
140 | 222 | } |
141 | 223 |
|
| 224 | + #[Test] |
| 225 | + public function parserErrorsThrownAfterInitializationInOpeningTagsAreHandledAsTextByTolerantErrorHandler(): void |
| 226 | + { |
| 227 | + $mockInterceptor = self::createMock(InterceptorInterface::class); |
| 228 | + $mockInterceptor->expects(self::once()) |
| 229 | + ->method('process') |
| 230 | + ->willThrowException(new ParserException('interceptor failure')); |
| 231 | + $mockInterceptor->expects(self::once()) |
| 232 | + ->method('getInterceptionPoints') |
| 233 | + ->willReturn([InterceptorInterface::INTERCEPT_CLOSING_VIEWHELPER]); |
| 234 | + |
| 235 | + $renderingContext = new ParserConfigurationAccessRenderingContext(); |
| 236 | + $renderingContext->setErrorHandler(new TolerantErrorHandler()); |
| 237 | + $renderingContext->parserConfiguration->addInterceptor($mockInterceptor); |
| 238 | + $renderingContext->getViewHelperResolver()->addNamespace('test', 'TYPO3Fluid\Fluid\Tests\Functional\Fixtures\ViewHelpers'); |
| 239 | + $subject = $renderingContext->getTemplateParser(); |
| 240 | + |
| 241 | + $parsedTemplate = $subject->parse('<test:requiredArgument required="test" />'); |
| 242 | + |
| 243 | + /** @var TextNode */ |
| 244 | + $textNode = $parsedTemplate->getRootNode()->getChildNodes()[0]; |
| 245 | + self::assertInstanceOf(TextNode::class, $textNode); |
| 246 | + self::assertStringContainsString( |
| 247 | + 'Parser error: interceptor failure Offending code: ', |
| 248 | + $textNode->getText(), |
| 249 | + ); |
| 250 | + } |
| 251 | + |
| 252 | + #[Test] |
| 253 | + public function viewHelperErrorsThrownAfterInitializationInOpeningTagsAreHandledAsTextByTolerantErrorHandler(): void |
| 254 | + { |
| 255 | + $mockInterceptor = self::createMock(InterceptorInterface::class); |
| 256 | + $mockInterceptor->expects(self::once()) |
| 257 | + ->method('process') |
| 258 | + ->willThrowException(new ViewHelperException('interceptor failure')); |
| 259 | + $mockInterceptor->expects(self::once()) |
| 260 | + ->method('getInterceptionPoints') |
| 261 | + ->willReturn([InterceptorInterface::INTERCEPT_CLOSING_VIEWHELPER]); |
| 262 | + |
| 263 | + $renderingContext = new ParserConfigurationAccessRenderingContext(); |
| 264 | + $renderingContext->setErrorHandler(new TolerantErrorHandler()); |
| 265 | + $renderingContext->parserConfiguration->addInterceptor($mockInterceptor); |
| 266 | + $renderingContext->getViewHelperResolver()->addNamespace('test', 'TYPO3Fluid\Fluid\Tests\Functional\Fixtures\ViewHelpers'); |
| 267 | + $subject = $renderingContext->getTemplateParser(); |
| 268 | + |
| 269 | + $parsedTemplate = $subject->parse('<test:requiredArgument required="test" />'); |
| 270 | + |
| 271 | + /** @var TextNode */ |
| 272 | + $textNode = $parsedTemplate->getRootNode()->getChildNodes()[0]; |
| 273 | + self::assertInstanceOf(TextNode::class, $textNode); |
| 274 | + self::assertStringContainsString( |
| 275 | + 'ViewHelper error: interceptor failure - Offending code: ', |
| 276 | + $textNode->getText(), |
| 277 | + ); |
| 278 | + } |
| 279 | + |
142 | 280 | public static function viewHelperArgumentsGetParsedCorrectlyDataProvider(): iterable |
143 | 281 | { |
144 | 282 | yield ['<test:arbitraryArguments />', []]; |
@@ -357,6 +495,21 @@ public function objectAccessorNodesAreNotEscapedIfEscapingIsDisabled(): void |
357 | 495 | self::assertSame('foo.bar', $objectAccessorNode->getObjectPath()); |
358 | 496 | } |
359 | 497 |
|
| 498 | + #[Test] |
| 499 | + public function parseResetsEscapingStateBetweenCalls(): void |
| 500 | + { |
| 501 | + $renderingContext = new RenderingContext(); |
| 502 | + $subject = $renderingContext->getTemplateParser(); |
| 503 | + |
| 504 | + $subject->parse('{escaping=false}{foo.bar}'); |
| 505 | + $parsedTemplate = $subject->parse('{foo.bar}'); |
| 506 | + |
| 507 | + /** @var EscapingNode */ |
| 508 | + $escapingNode = $parsedTemplate->getRootNode()->getChildNodes()[0]; |
| 509 | + self::assertInstanceOf(EscapingNode::class, $escapingNode); |
| 510 | + self::assertInstanceOf(ObjectAccessorNode::class, $escapingNode->getNode()); |
| 511 | + } |
| 512 | + |
360 | 513 | #[Test] |
361 | 514 | public function objectAccessorNodesAreRunThroughInterceptors(): void |
362 | 515 | { |
@@ -435,8 +588,14 @@ public function getOrParseAndStoreTemplateSetsAndStoresUncompilableStateInCache( |
435 | 588 | // Second try with uncompilable flag |
436 | 589 | '', |
437 | 590 | ); |
| 591 | + $mockErrorHandler = $this->createMock(ErrorHandlerInterface::class); |
| 592 | + $mockErrorHandler->expects(self::once()) |
| 593 | + ->method('handleCompilerError') |
| 594 | + ->with(self::isInstanceOf(StopCompilingException::class)) |
| 595 | + ->willReturn(''); |
438 | 596 |
|
439 | 597 | $renderingContext = new RenderingContext(); |
| 598 | + $renderingContext->setErrorHandler($mockErrorHandler); |
440 | 599 | $renderingContext->setTemplateCompiler($mockCompiler); |
441 | 600 | $subject = $renderingContext->getTemplateParser(); |
442 | 601 | $parsedTemplate = $subject->getOrParseAndStoreTemplate( |
|
0 commit comments