1a246d896f2215df628fe53b71fe91745e49d1e4
[Packages/TYPO3.CMS.git] / typo3 / sysext / felogin / Tests / Unit / Controller / FrontendLoginControllerTest.php
1 <?php
2 declare(strict_types = 1);
3 namespace TYPO3\CMS\Felogin\Tests\Unit\Controller;
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 Prophecy\Prophecy\ObjectProphecy;
20 use Psr\Log\NullLogger;
21 use TYPO3\CMS\Core\Authentication\LoginType;
22 use TYPO3\CMS\Core\Database\Connection;
23 use TYPO3\CMS\Core\Database\ConnectionPool;
24 use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
25 use TYPO3\CMS\Core\Database\Query\QueryBuilder;
26 use TYPO3\CMS\Core\Exception\SiteNotFoundException;
27 use TYPO3\CMS\Core\Site\SiteFinder;
28 use TYPO3\CMS\Core\Tests\Unit\Database\Mocks\MockPlatform;
29 use TYPO3\CMS\Core\Utility\GeneralUtility;
30 use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
31 use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
32 use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
33
34 /**
35 * Test case
36 */
37 class FrontendLoginControllerTest extends UnitTestCase
38 {
39 /**
40 * @var \TYPO3\CMS\Felogin\Controller\FrontendLoginController|\TYPO3\TestingFramework\Core\AccessibleObjectInterface
41 */
42 protected $accessibleFixture;
43
44 /**
45 * @var string
46 */
47 protected $testHostName;
48
49 /**
50 * @var string
51 */
52 protected $testSitePath;
53
54 /**
55 * @var string
56 */
57 protected $testTableName;
58
59 /**
60 * Set up
61 */
62 protected function setUp()
63 {
64 $GLOBALS['TSFE'] = new \stdClass();
65 $this->testTableName = 'sys_domain';
66 $this->testHostName = 'hostname.tld';
67 $this->testSitePath = '/';
68 $this->accessibleFixture = $this->getAccessibleMock(\TYPO3\CMS\Felogin\Controller\FrontendLoginController::class, ['dummy']);
69 $this->accessibleFixture->cObj = $this->createMock(\TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::class);
70 $this->accessibleFixture->_set('frontendController', $this->createMock(\TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController::class));
71 $this->accessibleFixture->setLogger(new NullLogger());
72
73 $mockedSiteFinder = $this->getAccessibleMock(SiteFinder::class, ['getSiteByPageId'], [], '', false, false);
74 $mockedSiteFinder->method('getSiteByPageId')->willThrowException(new SiteNotFoundException('Site not found', 1536819047));
75 $this->accessibleFixture->_set('siteFinder', $mockedSiteFinder);
76
77 $this->setUpFakeSitePathAndHost();
78 }
79
80 /**
81 * Tear down
82 */
83 protected function tearDown()
84 {
85 // setUpDatabaseMock() prepares some instances via addInstance(), but not all
86 // tests use that instance. purgeInstances() removes left overs
87 GeneralUtility::purgeInstances();
88 parent::tearDown();
89 }
90
91 /**
92 * Set up a fake site path and host
93 */
94 protected function setUpFakeSitePathAndHost()
95 {
96 $_SERVER['ORIG_PATH_INFO'] = $_SERVER['PATH_INFO'] = $_SERVER['ORIG_SCRIPT_NAME'] = $_SERVER['SCRIPT_NAME'] = $this->testSitePath . TYPO3_mainDir;
97 $_SERVER['HTTP_HOST'] = $this->testHostName;
98 }
99
100 /**
101 * Mock database
102 */
103 protected function setUpDatabaseMock()
104 {
105 /** @var Connection|ObjectProphecy $connection */
106 $connection = $this->prophesize(Connection::class);
107 $connection->getDatabasePlatform()->willReturn(new MockPlatform());
108 $connection->getExpressionBuilder()->willReturn(new ExpressionBuilder($connection->reveal()));
109 $connection->quoteIdentifier(Argument::cetera())->willReturnArgument(0);
110
111 // TODO: This should rather be a functional test if we need a query builder
112 // or we should clean up the code itself to not need to mock internal behavior here
113 $queryBuilder = new QueryBuilder(
114 $connection->reveal(),
115 null,
116 new \Doctrine\DBAL\Query\QueryBuilder($connection->reveal())
117 );
118
119 /** @var \Doctrine\DBAL\Driver\Statement|ObjectProphecy $resultSet */
120 $resultSet = $this->prophesize(\Doctrine\DBAL\Driver\Statement::class);
121 $resultSet->fetchAll()->willReturn([
122 ['domainName' => 'domainhostname.tld'],
123 ['domainName' => 'otherhostname.tld/path'],
124 ['domainName' => 'sub.domainhostname.tld/path/']
125 ]);
126
127 /** @var ConnectionPool|ObjectProphecy $connectionPool */
128 $connectionPool = $this->prophesize(ConnectionPool::class);
129 $connectionPool->getQueryBuilderForTable('sys_domain')->willReturn($queryBuilder);
130 GeneralUtility::addInstance(ConnectionPool::class, $connectionPool->reveal());
131
132 $connection->executeQuery('SELECT domainName FROM sys_domain', Argument::cetera())
133 ->willReturn($resultSet->reveal());
134 }
135
136 /**
137 * @test
138 */
139 public function typo3SitePathEqualsStubSitePath()
140 {
141 $this->assertEquals(\TYPO3\CMS\Core\Utility\GeneralUtility::getIndpEnv('TYPO3_SITE_PATH'), $this->testSitePath);
142 }
143
144 /**
145 * @test
146 */
147 public function typo3SiteUrlEqualsStubSiteUrl()
148 {
149 $this->assertEquals(\TYPO3\CMS\Core\Utility\GeneralUtility::getIndpEnv('TYPO3_SITE_URL'), ('http://' . $this->testHostName) . $this->testSitePath);
150 }
151
152 /**
153 * @test
154 */
155 public function typo3SitePathEqualsStubSitePathAfterChangingInTest()
156 {
157 $this->testHostName = 'somenewhostname.com';
158 $this->testSitePath = '/somenewpath/';
159 $this->setUpFakeSitePathAndHost();
160 $this->assertEquals(\TYPO3\CMS\Core\Utility\GeneralUtility::getIndpEnv('TYPO3_SITE_PATH'), $this->testSitePath);
161 }
162
163 /**
164 * @test
165 */
166 public function typo3SiteUrlEqualsStubSiteUrlAfterChangingInTest()
167 {
168 $this->testHostName = 'somenewhostname.com';
169 $this->testSitePath = '/somenewpath/';
170 $this->setUpFakeSitePathAndHost();
171 $this->assertEquals(\TYPO3\CMS\Core\Utility\GeneralUtility::getIndpEnv('TYPO3_SITE_URL'), ('http://' . $this->testHostName) . $this->testSitePath);
172 }
173
174 /**
175 * Data provider for validateRedirectUrlClearsUrl
176 *
177 * @return array
178 */
179 public function validateRedirectUrlClearsUrlDataProvider()
180 {
181 return [
182 'absolute URL, hostname not in sys_domain, trailing slash' => ['http://badhost.tld/'],
183 'absolute URL, hostname not in sys_domain, no trailing slash' => ['http://badhost.tld'],
184 'absolute URL, subdomain in sys_domain, but main domain not, trailing slash' => ['http://domainhostname.tld.badhost.tld/'],
185 'absolute URL, subdomain in sys_domain, but main domain not, no trailing slash' => ['http://domainhostname.tld.badhost.tld'],
186 'non http absolute URL 1' => ['its://domainhostname.tld/itunes/'],
187 'non http absolute URL 2' => ['ftp://domainhostname.tld/download/'],
188 'XSS attempt 1' => ['javascript:alert(123)'],
189 'XSS attempt 2' => ['" onmouseover="alert(123)"'],
190 'invalid URL, HTML break out attempt' => ['" >blabuubb'],
191 'invalid URL, UNC path' => ['\\\\foo\\bar\\'],
192 'invalid URL, backslashes in path' => ['http://domainhostname.tld\\bla\\blupp'],
193 'invalid URL, linefeed in path' => ['http://domainhostname.tld/bla/blupp' . LF],
194 'invalid URL, only one slash after scheme' => ['http:/domainhostname.tld/bla/blupp'],
195 'invalid URL, illegal chars' => ['http://(<>domainhostname).tld/bla/blupp'],
196 ];
197 }
198
199 /**
200 * @test
201 * @dataProvider validateRedirectUrlClearsUrlDataProvider
202 * @param string $url Invalid Url
203 */
204 public function validateRedirectUrlClearsUrl($url)
205 {
206 $this->setUpDatabaseMock();
207 $this->assertEquals('', $this->accessibleFixture->_call('validateRedirectUrl', $url));
208 }
209
210 /**
211 * Data provider for validateRedirectUrlKeepsCleanUrl
212 *
213 * @return array
214 */
215 public function validateRedirectUrlKeepsCleanUrlDataProvider()
216 {
217 return [
218 'sane absolute URL' => ['http://domainhostname.tld/'],
219 'sane absolute URL with script' => ['http://domainhostname.tld/index.php?id=1'],
220 'sane absolute URL with realurl' => ['http://domainhostname.tld/foo/bar/foo.html'],
221 'sane absolute URL with homedir' => ['http://domainhostname.tld/~user/'],
222 'sane absolute URL with some strange chars encoded' => ['http://domainhostname.tld/~user/a%cc%88o%cc%88%c3%9fa%cc%82/foo.html'],
223 'sane absolute URL (domain record with path)' => ['http://otherhostname.tld/path/'],
224 'sane absolute URL with script (domain record with path)' => ['http://otherhostname.tld/path/index.php?id=1'],
225 'sane absolute URL with realurl (domain record with path)' => ['http://otherhostname.tld/path/foo/bar/foo.html'],
226 'sane absolute URL (domain record with path and slash)' => ['http://sub.domainhostname.tld/path/'],
227 'sane absolute URL with script (domain record with path slash)' => ['http://sub.domainhostname.tld/path/index.php?id=1'],
228 'sane absolute URL with realurl (domain record with path slash)' => ['http://sub.domainhostname.tld/path/foo/bar/foo.html'],
229 'relative URL, no leading slash 1' => ['index.php?id=1'],
230 'relative URL, no leading slash 2' => ['foo/bar/index.php?id=2'],
231 'relative URL, leading slash, no realurl' => ['/index.php?id=1'],
232 'relative URL, leading slash, realurl' => ['/de/service/imprint.html'],
233 ];
234 }
235
236 /**
237 * @test
238 * @dataProvider validateRedirectUrlKeepsCleanUrlDataProvider
239 * @param string $url Clean URL to test
240 */
241 public function validateRedirectUrlKeepsCleanUrl($url)
242 {
243 $this->setUpDatabaseMock();
244 $this->assertEquals($url, $this->accessibleFixture->_call('validateRedirectUrl', $url));
245 }
246
247 /**
248 * Data provider for validateRedirectUrlClearsInvalidUrlInSubdirectory
249 *
250 * @return array
251 */
252 public function validateRedirectUrlClearsInvalidUrlInSubdirectoryDataProvider()
253 {
254 return [
255 'absolute URL, missing subdirectory' => ['http://hostname.tld/'],
256 'absolute URL, wrong subdirectory' => ['http://hostname.tld/hacker/index.php'],
257 'absolute URL, correct subdirectory, no trailing slash' => ['http://hostname.tld/subdir'],
258 'absolute URL, correct subdirectory of sys_domain record, no trailing slash' => ['http://otherhostname.tld/path'],
259 'absolute URL, correct subdirectory of sys_domain record, no trailing slash, subdomain' => ['http://sub.domainhostname.tld/path'],
260 'relative URL, leading slash, no path' => ['/index.php?id=1'],
261 'relative URL, leading slash, wrong path' => ['/de/sub/site.html'],
262 'relative URL, leading slash, slash only' => ['/'],
263 ];
264 }
265
266 /**
267 * @test
268 * @dataProvider validateRedirectUrlClearsInvalidUrlInSubdirectoryDataProvider
269 * @param string $url Invalid Url
270 */
271 public function validateRedirectUrlClearsInvalidUrlInSubdirectory($url)
272 {
273 $this->testSitePath = '/subdir/';
274 $this->setUpFakeSitePathAndHost();
275 $this->setUpDatabaseMock();
276 $this->assertEquals('', $this->accessibleFixture->_call('validateRedirectUrl', $url));
277 }
278
279 /**
280 * Data provider for validateRedirectUrlKeepsCleanUrlInSubdirectory
281 *
282 * @return array
283 */
284 public function validateRedirectUrlKeepsCleanUrlInSubdirectoryDataProvider()
285 {
286 return [
287 'absolute URL, correct subdirectory' => ['http://hostname.tld/subdir/'],
288 'absolute URL, correct subdirectory, realurl' => ['http://hostname.tld/subdir/de/imprint.html'],
289 'absolute URL, correct subdirectory, no realurl' => ['http://hostname.tld/subdir/index.php?id=10'],
290 'absolute URL, correct subdirectory of sys_domain record' => ['http://otherhostname.tld/path/'],
291 'absolute URL, correct subdirectory of sys_domain record, subdomain' => ['http://sub.domainhostname.tld/path/'],
292 'relative URL, no leading slash, realurl' => ['de/service/imprint.html'],
293 'relative URL, no leading slash, no realurl' => ['index.php?id=1'],
294 'relative nested URL, no leading slash, no realurl' => ['foo/bar/index.php?id=2']
295 ];
296 }
297
298 /**
299 * @test
300 * @dataProvider validateRedirectUrlKeepsCleanUrlInSubdirectoryDataProvider
301 * @param string $url Invalid Url
302 */
303 public function validateRedirectUrlKeepsCleanUrlInSubdirectory($url)
304 {
305 $this->testSitePath = '/subdir/';
306 $this->setUpFakeSitePathAndHost();
307 $this->setUpDatabaseMock();
308 $this->assertEquals($url, $this->accessibleFixture->_call('validateRedirectUrl', $url));
309 }
310
311 /*************************
312 * Test concerning getPreverveGetVars
313 *************************/
314
315 /**
316 * @return array
317 */
318 public function getPreserveGetVarsReturnsCorrectResultDataProvider()
319 {
320 return [
321 'special get var id is not preserved' => [
322 [
323 'id' => 42,
324 ],
325 '',
326 [],
327 ],
328 'simple additional parameter is not preserved if not specified in preservedGETvars' => [
329 [
330 'id' => 42,
331 'special' => 23,
332 ],
333 '',
334 [],
335 ],
336 'all params except ignored ones are preserved if preservedGETvars is set to "all"' => [
337 [
338 'id' => 42,
339 'special1' => 23,
340 'special2' => [
341 'foo' => 'bar',
342 ],
343 'tx_felogin_pi1' => [
344 'forgot' => 1,
345 ],
346 ],
347 'all',
348 [
349 'special1' => 23,
350 'special2' => [
351 'foo' => 'bar',
352 ],
353 ]
354 ],
355 'preserve single parameter' => [
356 [
357 'L' => 42,
358 ],
359 'L',
360 [
361 'L' => 42,
362 ],
363 ],
364 'preserve whole parameter array' => [
365 [
366 'L' => 3,
367 'tx_someext' => [
368 'foo' => 'simple',
369 'bar' => [
370 'baz' => 'simple',
371 ],
372 ],
373 ],
374 'L,tx_someext',
375 [
376 'L' => 3,
377 'tx_someext' => [
378 'foo' => 'simple',
379 'bar' => [
380 'baz' => 'simple',
381 ],
382 ],
383 ],
384 ],
385 'preserve part of sub array' => [
386 [
387 'L' => 3,
388 'tx_someext' => [
389 'foo' => 'simple',
390 'bar' => [
391 'baz' => 'simple',
392 ],
393 ],
394 ],
395 'L,tx_someext[bar]',
396 [
397 'L' => 3,
398 'tx_someext' => [
399 'bar' => [
400 'baz' => 'simple',
401 ],
402 ],
403 ],
404 ],
405 'preserve keys on different levels' => [
406 [
407 'L' => 3,
408 'no-preserve' => 'whatever',
409 'tx_ext2' => [
410 'foo' => 'simple',
411 ],
412 'tx_ext3' => [
413 'bar' => [
414 'baz' => 'simple',
415 ],
416 'go-away' => '',
417 ],
418 ],
419 'L,tx_ext2,tx_ext3[bar]',
420 [
421 'L' => 3,
422 'tx_ext2' => [
423 'foo' => 'simple',
424 ],
425 'tx_ext3' => [
426 'bar' => [
427 'baz' => 'simple',
428 ],
429 ],
430 ],
431 ],
432 'preserved value that does not exist in get' => [
433 [],
434 'L,foo%5Bbar%5D',
435 [],
436 ],
437 ];
438 }
439
440 /**
441 * @test
442 * @dataProvider getPreserveGetVarsReturnsCorrectResultDataProvider
443 * @param array $getArray
444 * @param string $preserveVars
445 * @param string $expected
446 */
447 public function getPreserveGetVarsReturnsCorrectResult(array $getArray, $preserveVars, $expected)
448 {
449 $_GET = $getArray;
450 $this->accessibleFixture->conf['preserveGETvars'] = $preserveVars;
451 $this->assertSame($expected, $this->accessibleFixture->_call('getPreserveGetVars'));
452 }
453
454 /**************************************************
455 * Tests concerning isInLocalDomain
456 **************************************************/
457
458 /**
459 * Dataprovider for isInCurrentDomainIgnoresScheme
460 *
461 * @return array
462 */
463 public function isInCurrentDomainIgnoresSchemeDataProvider()
464 {
465 return [
466 'url https, current host http' => [
467 'example.com', // HTTP_HOST
468 '0', // HTTPS
469 'https://example.com/foo.html' // URL
470 ],
471 'url http, current host https' => [
472 'example.com',
473 '1',
474 'http://example.com/foo.html'
475 ],
476 'url https, current host https' => [
477 'example.com',
478 '1',
479 'https://example.com/foo.html'
480 ],
481 'url http, current host http' => [
482 'example.com',
483 '0',
484 'http://example.com/foo.html'
485 ]
486 ];
487 }
488
489 /**
490 * @test
491 * @dataProvider isInCurrentDomainIgnoresSchemeDataProvider
492 * @param string $host $_SERVER['HTTP_HOST']
493 * @param string $https $_SERVER['HTTPS']
494 * @param string $url The url to test
495 */
496 public function isInCurrentDomainIgnoresScheme($host, $https, $url)
497 {
498 $_SERVER['HTTP_HOST'] = $host;
499 $_SERVER['HTTPS'] = $https;
500 $this->assertTrue($this->accessibleFixture->_call('isInCurrentDomain', $url));
501 }
502
503 /**
504 * @return array
505 */
506 public function isInCurrentDomainReturnsFalseIfDomainsAreDifferentDataProvider()
507 {
508 return [
509 'simple difference' => [
510 'example.com', // HTTP_HOST
511 'http://typo3.org/foo.html' // URL
512 ],
513 'subdomain different' => [
514 'example.com',
515 'http://foo.example.com/bar.html'
516 ]
517 ];
518 }
519
520 /**
521 * @test
522 * @dataProvider isInCurrentDomainReturnsFalseIfDomainsAreDifferentDataProvider
523 * @param string $host $_SERVER['HTTP_HOST']
524 * @param string $url The url to test
525 */
526 public function isInCurrentDomainReturnsFalseIfDomainsAreDifferent($host, $url)
527 {
528 $_SERVER['HTTP_HOST'] = $host;
529 $this->assertFalse($this->accessibleFixture->_call('isInCurrentDomain', $url));
530 }
531
532 /**
533 * @test
534 */
535 public function processRedirectReferrerDomainsMatchesDomains()
536 {
537 $conf = [
538 'redirectMode' => 'refererDomains',
539 'domains' => 'example.com'
540 ];
541
542 $this->accessibleFixture->_set('conf', $conf);
543 $this->accessibleFixture->_set('logintype', LoginType::LOGIN);
544 $this->accessibleFixture->_set('referer', 'http://www.example.com/snafu');
545 /** @var TypoScriptFrontendController $tsfe */
546 $tsfe = $this->accessibleFixture->_get('frontendController');
547 $this->accessibleFixture->_set('userIsLoggedIn', true);
548 $this->assertSame(['http://www.example.com/snafu'], $this->accessibleFixture->_call('processRedirect'));
549 }
550
551 /**
552 *
553 */
554 public function processUserFieldsRespectsDefaultConfigurationForStdWrapDataProvider()
555 {
556 return [
557 'Simple casing' => [
558 [
559 'username' => 'Holy',
560 'lastname' => 'Wood',
561 ],
562 [
563 'username.' => [
564 'case' => 'upper'
565 ]
566 ],
567 [
568 '###FEUSER_USERNAME###' => 'HOLY',
569 '###FEUSER_LASTNAME###' => 'Wood',
570 '###USER###' => 'HOLY'
571 ]
572 ],
573 'Default config applies' => [
574 [
575 'username' => 'Holy',
576 'lastname' => 'O" Mally',
577 ],
578 [
579 'username.' => [
580 'case' => 'upper'
581 ]
582 ],
583 [
584 '###FEUSER_USERNAME###' => 'HOLY',
585 '###FEUSER_LASTNAME###' => 'O&quot; Mally',
586 '###USER###' => 'HOLY'
587 ]
588 ],
589 'Specific config overrides default config' => [
590 [
591 'username' => 'Holy',
592 'lastname' => 'O" Mally',
593 ],
594 [
595 'username.' => [
596 'case' => 'upper'
597 ],
598 'lastname.' => [
599 'htmlSpecialChars' => '0'
600 ]
601 ],
602 [
603 '###FEUSER_USERNAME###' => 'HOLY',
604 '###FEUSER_LASTNAME###' => 'O" Mally',
605 '###USER###' => 'HOLY'
606 ]
607 ],
608 'No given user returns empty array' => [
609 null,
610 [
611 'username.' => [
612 'case' => 'upper'
613 ],
614 'lastname.' => [
615 'htmlSpecialChars' => '0'
616 ]
617 ],
618 []
619 ],
620 ];
621 }
622
623 /**
624 * @test
625 * @dataProvider processUserFieldsRespectsDefaultConfigurationForStdWrapDataProvider
626 */
627 public function processUserFieldsRespectsDefaultConfigurationForStdWrap($userRecord, $fieldConf, $expectedMarkers)
628 {
629 $tsfe = new \stdClass();
630 $tsfe->fe_user = new \stdClass();
631 $tsfe->fe_user->user = $userRecord;
632 $conf = ['userfields.' => $fieldConf];
633 $this->accessibleFixture->_set('cObj', new ContentObjectRenderer());
634 $this->accessibleFixture->_set('frontendController', $tsfe);
635 $this->accessibleFixture->_set('conf', $conf);
636 $actualResult = $this->accessibleFixture->_call('getUserFieldMarkers');
637 $this->assertEquals($expectedMarkers, $actualResult);
638 }
639 }