358acb0083f2c88b0496da650215ebcbb4ff9eeb
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Tests / Unit / Configuration / TypoScript / ConditionMatching / AbstractConditionMatcherTest.php
1 <?php
2 declare(strict_types = 1);
3 namespace TYPO3\CMS\Core\Tests\Unit\Configuration\TypoScript\ConditionMatching;
4
5 /*
6 * This file is part of the TYPO3 CMS project.
7 *
8 * It is free software; you can redistribute it and/or modify it under
9 * the terms of the GNU General Public License, either version 2
10 * of the License, or any later version.
11 *
12 * For the full copyright and license information, please read the
13 * LICENSE.txt file that was distributed with this source code.
14 *
15 * The TYPO3 project - inspiring people to share!
16 */
17
18 use Prophecy\Argument;
19 use TYPO3\CMS\Backend\Configuration\TypoScript\ConditionMatching\ConditionMatcher;
20 use TYPO3\CMS\Core\Cache\CacheManager;
21 use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
22 use TYPO3\CMS\Core\Configuration\TypoScript\ConditionMatching\AbstractConditionMatcher;
23 use TYPO3\CMS\Core\Context\Context;
24 use TYPO3\CMS\Core\Context\DateTimeAspect;
25 use TYPO3\CMS\Core\Core\ApplicationContext;
26 use TYPO3\CMS\Core\Http\ServerRequest;
27 use TYPO3\CMS\Core\Log\Logger;
28 use TYPO3\CMS\Core\Package\PackageInterface;
29 use TYPO3\CMS\Core\Package\PackageManager;
30 use TYPO3\CMS\Core\Utility\GeneralUtility;
31 use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
32
33 /**
34 * Test cases
35 */
36 class AbstractConditionMatcherTest extends UnitTestCase
37 {
38 /**
39 * @var ApplicationContext
40 */
41 protected $backupApplicationContext;
42
43 /**
44 * @var AbstractConditionMatcher|\PHPUnit_Framework_MockObject_MockObject
45 */
46 protected $conditionMatcher;
47
48 /**
49 * @var \ReflectionMethod
50 */
51 protected $evaluateConditionCommonMethod;
52
53 /**
54 * @var \ReflectionMethod
55 */
56 protected $evaluateExpressionMethod;
57
58 /**
59 * Set up
60 */
61 protected function setUp(): void
62 {
63 require_once 'Fixtures/ConditionMatcherUserFuncs.php';
64
65 $this->resetSingletonInstances = true;
66 $GLOBALS['TYPO3_REQUEST'] = new ServerRequest();
67 $cacheFrontendProphecy = $this->prophesize(FrontendInterface::class);
68 $cacheFrontendProphecy->has(Argument::any())->willReturn(false);
69 $cacheFrontendProphecy->set(Argument::any(), Argument::any())->willReturn(null);
70 $cacheManagerProphecy = $this->prophesize(CacheManager::class);
71 $cacheManagerProphecy->getCache('cache_core')->willReturn($cacheFrontendProphecy->reveal());
72 GeneralUtility::setSingletonInstance(CacheManager::class, $cacheManagerProphecy->reveal());
73
74 $packageManagerProphecy = $this->prophesize(PackageManager::class);
75 $corePackageProphecy = $this->prophesize(PackageInterface::class);
76 $corePackageProphecy->getPackagePath()->willReturn(__DIR__ . '/../../../../../../../sysext/core/');
77 $packageManagerProphecy->getActivePackages()->willReturn([
78 $corePackageProphecy->reveal()
79 ]);
80 GeneralUtility::setSingletonInstance(PackageManager::class, $packageManagerProphecy->reveal());
81
82 $this->initConditionMatcher();
83 $this->backupApplicationContext = GeneralUtility::getApplicationContext();
84 }
85
86 protected function initConditionMatcher()
87 {
88 // test the abstract methods via the backend condition matcher
89 $this->conditionMatcher = $this->getAccessibleMock(ConditionMatcher::class, ['determineRootline']);
90 $this->evaluateConditionCommonMethod = new \ReflectionMethod(AbstractConditionMatcher::class, 'evaluateConditionCommon');
91 $this->evaluateConditionCommonMethod->setAccessible(true);
92 $this->evaluateExpressionMethod = new \ReflectionMethod(AbstractConditionMatcher::class, 'evaluateExpression');
93 $this->evaluateExpressionMethod->setAccessible(true);
94 $loggerProphecy = $this->prophesize(Logger::class);
95 $this->conditionMatcher->setLogger($loggerProphecy->reveal());
96 }
97
98 /**
99 * Tear down
100 */
101 protected function tearDown(): void
102 {
103 Fixtures\GeneralUtilityFixture::setApplicationContext($this->backupApplicationContext);
104 parent::tearDown();
105 }
106
107 /**
108 * @return array
109 */
110 public function datesConditionDataProvider(): array
111 {
112 return [
113 '[dayofmonth = 17]' => ['dayofmonth', 17, true],
114 '[dayofweek = 3]' => ['dayofweek', 3, true],
115 '[dayofyear = 16]' => ['dayofyear', 16, true],
116 '[hour = 11]' => ['hour', 11, true],
117 '[minute = 4]' => ['minute', 4, true],
118 '[month = 1]' => ['month', 1, true],
119 '[year = 1945]' => ['year', 1945, true],
120 ];
121 }
122
123 /**
124 * @test
125 * @dataProvider datesConditionDataProvider
126 * @param string $expressionMethod
127 * @param int $expressionValue
128 * @param bool $expected
129 */
130 public function checkConditionMatcherForDates(string $expressionMethod, int $expressionValue, bool $expected): void
131 {
132 $GLOBALS['SIM_EXEC_TIME'] = mktime(11, 4, 0, 1, 17, 1945);
133 $this->assertSame($expected, $this->evaluateConditionCommonMethod->invokeArgs(
134 $this->conditionMatcher,
135 [$expressionMethod, $expressionValue]
136 ));
137 }
138
139 /**
140 * @return array
141 */
142 public function datesFunctionDataProvider(): array
143 {
144 return [
145 '[dayofmonth = 17]' => ['j', 17, true],
146 '[dayofweek = 3]' => ['w', 3, true],
147 '[dayofyear = 16]' => ['z', 16, true],
148 '[hour = 11]' => ['G', 11, true],
149 '[minute = 4]' => ['i', 4, true],
150 '[month = 1]' => ['n', 1, true],
151 '[year = 1945]' => ['Y', 1945, true],
152 ];
153 }
154
155 /**
156 * @test
157 * @dataProvider datesFunctionDataProvider
158 * @param string $format
159 * @param int $expressionValue
160 * @param bool $expected
161 */
162 public function checkConditionMatcherForDateFunction(string $format, int $expressionValue, bool $expected): void
163 {
164 $GLOBALS['SIM_EXEC_TIME'] = gmmktime(11, 4, 0, 1, 17, 1945);
165 GeneralUtility::makeInstance(Context::class)
166 ->setAspect('date', new DateTimeAspect(new \DateTimeImmutable('@' . $GLOBALS['SIM_EXEC_TIME'])));
167 $this->assertSame(
168 $expected,
169 $this->evaluateExpressionMethod->invokeArgs($this->conditionMatcher, ['date("' . $format . '") == ' . $expressionValue])
170 );
171 }
172
173 /**
174 * @return array
175 */
176 public function hostnameDataProvider(): array
177 {
178 return [
179 '[hostname = localhost]' => ['hostname', 'localhost', true],
180 '[hostname = localhost, foo.local]' => ['hostname', 'localhost, foo.local', true],
181 '[hostname = bar.local, foo.local]' => ['hostname', 'bar.local, foo.local', false],
182 ];
183 }
184
185 /**
186 * @test
187 * @dataProvider hostnameDataProvider
188 * @param string $expressionMethod
189 * @param string $expressionValue
190 * @param bool $expected
191 */
192 public function checkConditionMatcherForHostname(string $expressionMethod, string $expressionValue, bool $expected): void
193 {
194 $GLOBALS['_SERVER']['REMOTE_ADDR'] = '127.0.0.1';
195 $this->assertSame($expected, $this->evaluateConditionCommonMethod->invokeArgs(
196 $this->conditionMatcher,
197 [$expressionMethod, $expressionValue]
198 ));
199 }
200
201 /**
202 * Data provider with matching applicationContext conditions.
203 *
204 * @return array
205 */
206 public function matchingApplicationContextConditionsDataProvider(): array
207 {
208 return [
209 ['Production*'],
210 ['Production/Staging/*'],
211 ['Production/Staging/Server2'],
212 ['/^Production.*$/'],
213 ['/^Production\\/.+\\/Server\\d+$/'],
214 ];
215 }
216
217 /**
218 * @test
219 * @dataProvider matchingApplicationContextConditionsDataProvider
220 */
221 public function evaluateConditionCommonReturnsTrueForMatchingContexts($matchingContextCondition): void
222 {
223 /** @var ApplicationContext $applicationContext */
224 $applicationContext = new ApplicationContext('Production/Staging/Server2');
225 Fixtures\GeneralUtilityFixture::setApplicationContext($applicationContext);
226
227 $this->assertTrue(
228 $this->evaluateConditionCommonMethod->invokeArgs($this->conditionMatcher, ['applicationContext', $matchingContextCondition])
229 );
230 // Test expression language
231 $this->assertTrue(
232 $this->evaluateExpressionMethod->invokeArgs($this->conditionMatcher, ['like("' . $applicationContext . '", "' . preg_quote($matchingContextCondition, '/') . '")'])
233 );
234 }
235
236 /**
237 * Data provider with not matching applicationContext conditions.
238 *
239 * @return array
240 */
241 public function notMatchingApplicationContextConditionsDataProvider(): array
242 {
243 return [
244 ['Production'],
245 ['Testing*'],
246 ['Development/Profiling, Testing/Unit'],
247 ['Testing/Staging/Server2'],
248 ['/^Testing.*$/'],
249 ['/^Production\\/.+\\/Host\\d+$/'],
250 ];
251 }
252
253 /**
254 * @test
255 * @dataProvider notMatchingApplicationContextConditionsDataProvider
256 */
257 public function evaluateConditionCommonReturnsNullForNotMatchingApplicationContexts($notMatchingApplicationContextCondition): void
258 {
259 /** @var ApplicationContext $applicationContext */
260 $applicationContext = new ApplicationContext('Production/Staging/Server2');
261 Fixtures\GeneralUtilityFixture::setApplicationContext($applicationContext);
262
263 $this->assertFalse(
264 $this->evaluateConditionCommonMethod->invokeArgs($this->conditionMatcher, ['applicationContext', $notMatchingApplicationContextCondition])
265 );
266 // Test expression language
267 $this->assertFalse(
268 $this->evaluateExpressionMethod->invokeArgs($this->conditionMatcher, ['like("' . $applicationContext . '", "' . preg_quote($notMatchingApplicationContextCondition, '/') . '")'])
269 );
270 }
271
272 /**
273 * Data provider for evaluateConditionCommonEvaluatesIpAddressesCorrectly
274 *
275 * @return array
276 */
277 public function evaluateConditionCommonDevIpMaskDataProvider(): array
278 {
279 return [
280 // [0] $GLOBALS['TYPO3_CONF_VARS']['SYS']['devIPmask']
281 // [1] Actual IP
282 // [2] Expected condition result
283 'IP matches' => [
284 '127.0.0.1',
285 '127.0.0.1',
286 true,
287 ],
288 'ipv4 wildcard subnet' => [
289 '127.0.0.1/24',
290 '127.0.0.2',
291 true,
292 ],
293 'ipv6 wildcard subnet' => [
294 '0:0::1/128',
295 '::1',
296 true,
297 ],
298 'List of addresses matches' => [
299 '1.2.3.4, 5.6.7.8',
300 '5.6.7.8',
301 true,
302 ],
303 'IP does not match' => [
304 '127.0.0.1',
305 '127.0.0.2',
306 false,
307 ],
308 'ipv4 subnet does not match' => [
309 '127.0.0.1/8',
310 '126.0.0.1',
311 false,
312 ],
313 'ipv6 subnet does not match' => [
314 '::1/127',
315 '::2',
316 false
317 ],
318 'List of addresses does not match' => [
319 '127.0.0.1, ::1',
320 '::2',
321 false,
322 ],
323 ];
324 }
325
326 /**
327 * @test
328 * @dataProvider evaluateConditionCommonDevIpMaskDataProvider
329 */
330 public function evaluateConditionCommonEvaluatesIpAddressesCorrectly($devIpMask, $actualIp, $expectedResult): void
331 {
332 // Do not trigger proxy stuff of GeneralUtility::getIndPEnv
333 unset($GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP']);
334
335 GeneralUtility::setIndpEnv('REMOTE_ADDR', $actualIp);
336 $GLOBALS['TYPO3_CONF_VARS']['SYS']['devIPmask'] = $devIpMask;
337 $this->initConditionMatcher();
338 $result = $this->evaluateExpressionMethod->invokeArgs($this->conditionMatcher, ['ip("devIP")']);
339 $this->assertSame($expectedResult, $result);
340 }
341
342 /**
343 * @test
344 */
345 public function testUserFuncIsCalled(): void
346 {
347 $this->assertTrue(
348 $this->evaluateConditionCommonMethod->invokeArgs(
349 $this->conditionMatcher,
350 ['userFunc', 'user_testFunction']
351 )
352 );
353 }
354
355 /**
356 * @test
357 */
358 public function testUserFuncWithSingleArgument(): void
359 {
360 $this->assertTrue(
361 $this->evaluateConditionCommonMethod->invokeArgs(
362 $this->conditionMatcher,
363 ['userFunc', 'user_testFunctionWithSingleArgument(x)']
364 )
365 );
366 }
367
368 /**
369 * @test
370 */
371 public function testUserFuncWithIntegerZeroArgument(): void
372 {
373 $this->assertTrue(
374 $this->evaluateConditionCommonMethod->invokeArgs(
375 $this->conditionMatcher,
376 ['userFunc', 'user_testFunctionWithSingleArgument(0)']
377 )
378 );
379 }
380
381 /**
382 * @test
383 */
384 public function testUserFuncWithWhitespaceArgument(): void
385 {
386 $this->assertTrue(
387 $this->evaluateConditionCommonMethod->invokeArgs(
388 $this->conditionMatcher,
389 ['userFunc', 'user_testFunctionWithNoArgument( )']
390 )
391 );
392 }
393
394 /**
395 * @test
396 */
397 public function testUserFuncWithMultipleArguments(): void
398 {
399 $this->assertTrue(
400 $this->evaluateConditionCommonMethod->invokeArgs(
401 $this->conditionMatcher,
402 ['userFunc', 'user_testFunctionWithThreeArguments(1,2,3)']
403 )
404 );
405 }
406
407 /**
408 * @test
409 */
410 public function testUserFuncWithMultipleDifferentArgumentsNullBoolString(): void
411 {
412 $this->assertTrue(
413 $this->evaluateConditionCommonMethod->invokeArgs(
414 $this->conditionMatcher,
415 ['userFunc', 'user_testFunctionWithThreeArguments(0,true,"foo")']
416 )
417 );
418 }
419
420 /**
421 * @test
422 */
423 public function testUserFuncWithMultipleDifferentArgumentsNullStringBool(): void
424 {
425 $this->assertTrue(
426 $this->evaluateConditionCommonMethod->invokeArgs(
427 $this->conditionMatcher,
428 ['userFunc', 'user_testFunctionWithThreeArguments(0,"foo",true)']
429 )
430 );
431 }
432
433 /**
434 * @test
435 */
436 public function testUserFuncWithMultipleDifferentArgumentsStringBoolNull(): void
437 {
438 $this->assertTrue(
439 $this->evaluateConditionCommonMethod->invokeArgs(
440 $this->conditionMatcher,
441 ['userFunc', 'user_testFunctionWithThreeArguments("foo",true,0)']
442 )
443 );
444 }
445
446 /**
447 * @test
448 */
449 public function testUserFuncWithMultipleDifferentArgumentsStringNullBool(): void
450 {
451 $this->assertTrue(
452 $this->evaluateConditionCommonMethod->invokeArgs(
453 $this->conditionMatcher,
454 ['userFunc', 'user_testFunctionWithThreeArguments("foo",0,true)']
455 )
456 );
457 }
458
459 /**
460 * @test
461 */
462 public function testUserFuncWithMultipleDifferentArgumentsBoolNullString(): void
463 {
464 $this->assertTrue(
465 $this->evaluateConditionCommonMethod->invokeArgs(
466 $this->conditionMatcher,
467 ['userFunc', 'user_testFunctionWithThreeArguments(true,0,"foo")']
468 )
469 );
470 }
471
472 /**
473 * @test
474 */
475 public function testUserFuncWithMultipleDifferentArgumentsBoolStringNull(): void
476 {
477 $this->assertTrue(
478 $this->evaluateConditionCommonMethod->invokeArgs(
479 $this->conditionMatcher,
480 ['userFunc', 'user_testFunctionWithThreeArguments(true,"foo",0)']
481 )
482 );
483 }
484
485 /**
486 * @test
487 */
488 public function testUserFuncWithMultipleDifferentArgumentsNullBoolStringSingleQuotes(): void
489 {
490 $this->assertTrue(
491 $this->evaluateConditionCommonMethod->invokeArgs(
492 $this->conditionMatcher,
493 ['userFunc', "user_testFunctionWithThreeArguments(0,true,'foo')"]
494 )
495 );
496 }
497
498 /**
499 * @test
500 */
501 public function testUserFuncWithMultipleDifferentArgumentsNullStringBoolSingleQuotes(): void
502 {
503 $this->assertTrue(
504 $this->evaluateConditionCommonMethod->invokeArgs(
505 $this->conditionMatcher,
506 ['userFunc', "user_testFunctionWithThreeArguments(0,'foo',true)"]
507 )
508 );
509 }
510
511 /**
512 * @test
513 */
514 public function testUserFuncWithMultipleDifferentArgumentsStringBoolNullSingleQuotes(): void
515 {
516 $this->assertTrue(
517 $this->evaluateConditionCommonMethod->invokeArgs(
518 $this->conditionMatcher,
519 ['userFunc', "user_testFunctionWithThreeArguments('foo',true,0)"]
520 )
521 );
522 }
523
524 /**
525 * @test
526 */
527 public function testUserFuncWithMultipleDifferentArgumentsStringNullBoolSingleQuotes(): void
528 {
529 $this->assertTrue(
530 $this->evaluateConditionCommonMethod->invokeArgs(
531 $this->conditionMatcher,
532 ['userFunc', "user_testFunctionWithThreeArguments('foo',0,true)"]
533 )
534 );
535 }
536
537 /**
538 * @test
539 */
540 public function testUserFuncWithMultipleDifferentArgumentsBoolNullStringSingleQuotes(): void
541 {
542 $this->assertTrue(
543 $this->evaluateConditionCommonMethod->invokeArgs(
544 $this->conditionMatcher,
545 ['userFunc', "user_testFunctionWithThreeArguments(true,0,'foo')"]
546 )
547 );
548 }
549
550 /**
551 * @test
552 */
553 public function testUserFuncWithMultipleDifferentArgumentsBoolStringNullSingleQuotes(): void
554 {
555 $this->assertTrue(
556 $this->evaluateConditionCommonMethod->invokeArgs(
557 $this->conditionMatcher,
558 ['userFunc', "user_testFunctionWithThreeArguments(true,'foo',0)"]
559 )
560 );
561 }
562
563 /**
564 * @test
565 */
566 public function testUserFuncWithMultipleSingleQuotedArguments(): void
567 {
568 $this->assertTrue(
569 $this->evaluateConditionCommonMethod->invokeArgs(
570 $this->conditionMatcher,
571 ['userFunc', "user_testFunctionWithThreeArguments('foo','bar', 'baz')"]
572 )
573 );
574 }
575
576 /**
577 * @test
578 */
579 public function testUserFuncWithMultipleSoubleQuotedArguments(): void
580 {
581 $this->assertTrue(
582 $this->evaluateConditionCommonMethod->invokeArgs(
583 $this->conditionMatcher,
584 ['userFunc', 'user_testFunctionWithThreeArguments("foo","bar","baz")']
585 )
586 );
587 }
588
589 /**
590 * @test
591 */
592 public function testUserFuncReturnsFalse(): void
593 {
594 $this->assertFalse(
595 $this->evaluateConditionCommonMethod->invokeArgs(
596 $this->conditionMatcher,
597 ['userFunc', 'user_testFunctionFalse']
598 )
599 );
600 }
601
602 /**
603 * @test
604 */
605 public function testUserFuncWithMultipleArgumentsAndQuotes(): void
606 {
607 $this->assertTrue(
608 $this->evaluateConditionCommonMethod->invokeArgs(
609 $this->conditionMatcher,
610 ['userFunc', 'user_testFunctionWithThreeArguments(1,2,"3,4,5,6")']
611 )
612 );
613 }
614
615 /**
616 * @test
617 */
618 public function testUserFuncWithMultipleArgumentsAndQuotesAndSpaces(): void
619 {
620 $this->assertTrue(
621 $this->evaluateConditionCommonMethod->invokeArgs(
622 $this->conditionMatcher,
623 ['userFunc', 'user_testFunctionWithThreeArguments ( 1 , 2, "3, 4, 5, 6" )']
624 )
625 );
626 }
627
628 /**
629 * @test
630 */
631 public function testUserFuncWithMultipleArgumentsAndQuotesAndSpacesStripped(): void
632 {
633 $this->assertTrue(
634 $this->evaluateConditionCommonMethod->invokeArgs(
635 $this->conditionMatcher,
636 ['userFunc', 'user_testFunctionWithThreeArgumentsSpaces ( 1 , 2, "3, 4, 5, 6" )']
637 )
638 );
639 }
640
641 /**
642 * @test
643 */
644 public function testUserFuncWithSpacesInQuotes(): void
645 {
646 $this->assertTrue(
647 $this->evaluateConditionCommonMethod->invokeArgs(
648 $this->conditionMatcher,
649 ['userFunc', 'user_testFunctionWithSpaces(" 3, 4, 5, 6 ")']
650 )
651 );
652 }
653
654 /**
655 * @test
656 */
657 public function testUserFuncWithMultipleArgumentsAndQuotesAndSpacesStrippedAndEscapes(): void
658 {
659 $this->assertTrue(
660 $this->evaluateConditionCommonMethod->invokeArgs(
661 $this->conditionMatcher,
662 ['userFunc', 'user_testFunctionWithThreeArgumentsSpaces ( 1 , 2, "3, \"4, 5\", 6" )']
663 )
664 );
665 }
666
667 /**
668 * @test
669 */
670 public function testUserFuncWithQuoteMissing(): void
671 {
672 $this->assertTrue(
673 $this->evaluateConditionCommonMethod->invokeArgs(
674 $this->conditionMatcher,
675 ['userFunc', 'user_testFunctionWithQuoteMissing ("value \")']
676 )
677 );
678 }
679
680 /**
681 * @test
682 */
683 public function testUserFuncWithQuotesInside(): void
684 {
685 $this->assertTrue(
686 $this->evaluateConditionCommonMethod->invokeArgs(
687 $this->conditionMatcher,
688 ['userFunc', 'user_testQuotes("1 \" 2")']
689 )
690 );
691 }
692
693 /**
694 * @test
695 */
696 public function testUserFuncWithClassMethodCall(): void
697 {
698 $this->assertTrue(
699 $this->evaluateConditionCommonMethod->invokeArgs(
700 $this->conditionMatcher,
701 ['userFunc', 'ConditionMatcherUserFunctions::isTrue(1)']
702 )
703 );
704 }
705 }