36031047930806652864384c4aad3c4625584456
[Packages/TYPO3.CMS.git] / typo3 / sysext / felogin / Tests / Unit / Controller / FrontendLoginControllerTest.php
1 <?php
2 namespace TYPO3\CMS\Felogin\Tests\Unit\Controller;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use Prophecy\Argument;
18 use Prophecy\Prophecy\ObjectProphecy;
19 use Psr\Log\NullLogger;
20 use TYPO3\CMS\Core\Authentication\LoginType;
21 use TYPO3\CMS\Core\Database\Connection;
22 use TYPO3\CMS\Core\Database\ConnectionPool;
23 use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
24 use TYPO3\CMS\Core\Database\Query\QueryBuilder;
25 use TYPO3\CMS\Core\Tests\Unit\Database\Mocks\MockPlatform;
26 use TYPO3\CMS\Core\Utility\GeneralUtility;
27 use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
28
29 /**
30 * Test case
31 */
32 class FrontendLoginControllerTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
33 {
34 /**
35 * Subject is not notice free, disable E_NOTICES
36 */
37 protected static $suppressNotices = true;
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 $this->setUpFakeSitePathAndHost();
73 }
74
75 /**
76 * Tear down
77 */
78 protected function tearDown()
79 {
80 // setUpDatabaseMock() prepares some instances via addInstance(), but not all
81 // tests use that instance. purgeInstances() removes left overs
82 GeneralUtility::purgeInstances();
83 parent::tearDown();
84 }
85
86 /**
87 * Set up a fake site path and host
88 */
89 protected function setUpFakeSitePathAndHost()
90 {
91 GeneralUtility::flushInternalRuntimeCaches();
92 $_SERVER['ORIG_PATH_INFO'] = $_SERVER['PATH_INFO'] = $_SERVER['ORIG_SCRIPT_NAME'] = $_SERVER['SCRIPT_NAME'] = $this->testSitePath . TYPO3_mainDir;
93 $_SERVER['HTTP_HOST'] = $this->testHostName;
94 }
95
96 /**
97 * Mock database
98 */
99 protected function setUpDatabaseMock()
100 {
101 /** @var Connection|ObjectProphecy $connection */
102 $connection = $this->prophesize(Connection::class);
103 $connection->getDatabasePlatform()->willReturn(new MockPlatform());
104 $connection->getExpressionBuilder()->willReturn(new ExpressionBuilder($connection->reveal()));
105 $connection->quoteIdentifier(Argument::cetera())->willReturnArgument(0);
106
107 // TODO: This should rather be a functional test if we need a query builder
108 // or we should clean up the code itself to not need to mock internal behavior here
109 $queryBuilder = new QueryBuilder(
110 $connection->reveal(),
111 null,
112 new \Doctrine\DBAL\Query\QueryBuilder($connection->reveal())
113 );
114
115 /** @var \Doctrine\DBAL\Driver\Statement|ObjectProphecy $resultSet */
116 $resultSet = $this->prophesize(\Doctrine\DBAL\Driver\Statement::class);
117 $resultSet->fetchAll()->willReturn([
118 ['domainName' => 'domainhostname.tld'],
119 ['domainName' => 'otherhostname.tld/path'],
120 ['domainName' => 'sub.domainhostname.tld/path/']
121 ]);
122
123 /** @var ConnectionPool|ObjectProphecy $connectionPool */
124 $connectionPool = $this->prophesize(ConnectionPool::class);
125 $connectionPool->getQueryBuilderForTable('sys_domain')->willReturn($queryBuilder);
126 GeneralUtility::addInstance(ConnectionPool::class, $connectionPool->reveal());
127
128 $connection->executeQuery('SELECT domainName FROM sys_domain', Argument::cetera())
129 ->willReturn($resultSet->reveal());
130 }
131
132 /**
133 * @test
134 */
135 public function typo3SitePathEqualsStubSitePath()
136 {
137 $this->assertEquals(\TYPO3\CMS\Core\Utility\GeneralUtility::getIndpEnv('TYPO3_SITE_PATH'), $this->testSitePath);
138 }
139
140 /**
141 * @test
142 */
143 public function typo3SiteUrlEqualsStubSiteUrl()
144 {
145 $this->assertEquals(\TYPO3\CMS\Core\Utility\GeneralUtility::getIndpEnv('TYPO3_SITE_URL'), ('http://' . $this->testHostName) . $this->testSitePath);
146 }
147
148 /**
149 * @test
150 */
151 public function typo3SitePathEqualsStubSitePathAfterChangingInTest()
152 {
153 $this->testHostName = 'somenewhostname.com';
154 $this->testSitePath = '/somenewpath/';
155 $this->setUpFakeSitePathAndHost();
156 $this->assertEquals(\TYPO3\CMS\Core\Utility\GeneralUtility::getIndpEnv('TYPO3_SITE_PATH'), $this->testSitePath);
157 }
158
159 /**
160 * @test
161 */
162 public function typo3SiteUrlEqualsStubSiteUrlAfterChangingInTest()
163 {
164 $this->testHostName = 'somenewhostname.com';
165 $this->testSitePath = '/somenewpath/';
166 $this->setUpFakeSitePathAndHost();
167 $this->assertEquals(\TYPO3\CMS\Core\Utility\GeneralUtility::getIndpEnv('TYPO3_SITE_URL'), ('http://' . $this->testHostName) . $this->testSitePath);
168 }
169
170 /**
171 * Data provider for validateRedirectUrlClearsUrl
172 *
173 * @return array
174 */
175 public function validateRedirectUrlClearsUrlDataProvider()
176 {
177 return [
178 'absolute URL, hostname not in sys_domain, trailing slash' => ['http://badhost.tld/'],
179 'absolute URL, hostname not in sys_domain, no trailing slash' => ['http://badhost.tld'],
180 'absolute URL, subdomain in sys_domain, but main domain not, trailing slash' => ['http://domainhostname.tld.badhost.tld/'],
181 'absolute URL, subdomain in sys_domain, but main domain not, no trailing slash' => ['http://domainhostname.tld.badhost.tld'],
182 'non http absolute URL 1' => ['its://domainhostname.tld/itunes/'],
183 'non http absolute URL 2' => ['ftp://domainhostname.tld/download/'],
184 'XSS attempt 1' => ['javascript:alert(123)'],
185 'XSS attempt 2' => ['" onmouseover="alert(123)"'],
186 'invalid URL, HTML break out attempt' => ['" >blabuubb'],
187 'invalid URL, UNC path' => ['\\\\foo\\bar\\'],
188 'invalid URL, backslashes in path' => ['http://domainhostname.tld\\bla\\blupp'],
189 'invalid URL, linefeed in path' => ['http://domainhostname.tld/bla/blupp' . LF],
190 'invalid URL, only one slash after scheme' => ['http:/domainhostname.tld/bla/blupp'],
191 'invalid URL, illegal chars' => ['http://(<>domainhostname).tld/bla/blupp'],
192 ];
193 }
194
195 /**
196 * @test
197 * @dataProvider validateRedirectUrlClearsUrlDataProvider
198 * @param string $url Invalid Url
199 */
200 public function validateRedirectUrlClearsUrl($url)
201 {
202 $this->setUpDatabaseMock();
203 $this->assertEquals('', $this->accessibleFixture->_call('validateRedirectUrl', $url));
204 }
205
206 /**
207 * Data provider for validateRedirectUrlKeepsCleanUrl
208 *
209 * @return array
210 */
211 public function validateRedirectUrlKeepsCleanUrlDataProvider()
212 {
213 return [
214 'sane absolute URL' => ['http://domainhostname.tld/'],
215 'sane absolute URL with script' => ['http://domainhostname.tld/index.php?id=1'],
216 'sane absolute URL with realurl' => ['http://domainhostname.tld/foo/bar/foo.html'],
217 'sane absolute URL with homedir' => ['http://domainhostname.tld/~user/'],
218 'sane absolute URL with some strange chars encoded' => ['http://domainhostname.tld/~user/a%cc%88o%cc%88%c3%9fa%cc%82/foo.html'],
219 'sane absolute URL (domain record with path)' => ['http://otherhostname.tld/path/'],
220 'sane absolute URL with script (domain record with path)' => ['http://otherhostname.tld/path/index.php?id=1'],
221 'sane absolute URL with realurl (domain record with path)' => ['http://otherhostname.tld/path/foo/bar/foo.html'],
222 'sane absolute URL (domain record with path and slash)' => ['http://sub.domainhostname.tld/path/'],
223 'sane absolute URL with script (domain record with path slash)' => ['http://sub.domainhostname.tld/path/index.php?id=1'],
224 'sane absolute URL with realurl (domain record with path slash)' => ['http://sub.domainhostname.tld/path/foo/bar/foo.html'],
225 'relative URL, no leading slash 1' => ['index.php?id=1'],
226 'relative URL, no leading slash 2' => ['foo/bar/index.php?id=2'],
227 'relative URL, leading slash, no realurl' => ['/index.php?id=1'],
228 'relative URL, leading slash, realurl' => ['/de/service/imprint.html'],
229 ];
230 }
231
232 /**
233 * @test
234 * @dataProvider validateRedirectUrlKeepsCleanUrlDataProvider
235 * @param string $url Clean URL to test
236 */
237 public function validateRedirectUrlKeepsCleanUrl($url)
238 {
239 $this->setUpDatabaseMock();
240 $this->assertEquals($url, $this->accessibleFixture->_call('validateRedirectUrl', $url));
241 }
242
243 /**
244 * Data provider for validateRedirectUrlClearsInvalidUrlInSubdirectory
245 *
246 * @return array
247 */
248 public function validateRedirectUrlClearsInvalidUrlInSubdirectoryDataProvider()
249 {
250 return [
251 'absolute URL, missing subdirectory' => ['http://hostname.tld/'],
252 'absolute URL, wrong subdirectory' => ['http://hostname.tld/hacker/index.php'],
253 'absolute URL, correct subdirectory, no trailing slash' => ['http://hostname.tld/subdir'],
254 'absolute URL, correct subdirectory of sys_domain record, no trailing slash' => ['http://otherhostname.tld/path'],
255 'absolute URL, correct subdirectory of sys_domain record, no trailing slash, subdomain' => ['http://sub.domainhostname.tld/path'],
256 'relative URL, leading slash, no path' => ['/index.php?id=1'],
257 'relative URL, leading slash, wrong path' => ['/de/sub/site.html'],
258 'relative URL, leading slash, slash only' => ['/'],
259 ];
260 }
261
262 /**
263 * @test
264 * @dataProvider validateRedirectUrlClearsInvalidUrlInSubdirectoryDataProvider
265 * @param string $url Invalid Url
266 */
267 public function validateRedirectUrlClearsInvalidUrlInSubdirectory($url)
268 {
269 $this->testSitePath = '/subdir/';
270 $this->setUpFakeSitePathAndHost();
271 $this->setUpDatabaseMock();
272 $this->assertEquals('', $this->accessibleFixture->_call('validateRedirectUrl', $url));
273 }
274
275 /**
276 * Data provider for validateRedirectUrlKeepsCleanUrlInSubdirectory
277 *
278 * @return array
279 */
280 public function validateRedirectUrlKeepsCleanUrlInSubdirectoryDataProvider()
281 {
282 return [
283 'absolute URL, correct subdirectory' => ['http://hostname.tld/subdir/'],
284 'absolute URL, correct subdirectory, realurl' => ['http://hostname.tld/subdir/de/imprint.html'],
285 'absolute URL, correct subdirectory, no realurl' => ['http://hostname.tld/subdir/index.php?id=10'],
286 'absolute URL, correct subdirectory of sys_domain record' => ['http://otherhostname.tld/path/'],
287 'absolute URL, correct subdirectory of sys_domain record, subdomain' => ['http://sub.domainhostname.tld/path/'],
288 'relative URL, no leading slash, realurl' => ['de/service/imprint.html'],
289 'relative URL, no leading slash, no realurl' => ['index.php?id=1'],
290 'relative nested URL, no leading slash, no realurl' => ['foo/bar/index.php?id=2']
291 ];
292 }
293
294 /**
295 * @test
296 * @dataProvider validateRedirectUrlKeepsCleanUrlInSubdirectoryDataProvider
297 * @param string $url Invalid Url
298 */
299 public function validateRedirectUrlKeepsCleanUrlInSubdirectory($url)
300 {
301 $this->testSitePath = '/subdir/';
302 $this->setUpFakeSitePathAndHost();
303 $this->setUpDatabaseMock();
304 $this->assertEquals($url, $this->accessibleFixture->_call('validateRedirectUrl', $url));
305 }
306
307 /*************************
308 * Test concerning getPreverveGetVars
309 *************************/
310
311 /**
312 * @return array
313 */
314 public function getPreserveGetVarsReturnsCorrectResultDataProvider()
315 {
316 return [
317 'special get var id is not preserved' => [
318 [
319 'id' => 42,
320 ],
321 '',
322 '',
323 ],
324 'simple additional parameter is not preserved if not specified in preservedGETvars' => [
325 [
326 'id' => 42,
327 'special' => 23,
328 ],
329 '',
330 '',
331 ],
332 'all params except ignored ones are preserved if preservedGETvars is set to "all"' => [
333 [
334 'id' => 42,
335 'special1' => 23,
336 'special2' => [
337 'foo' => 'bar',
338 ],
339 'tx_felogin_pi1' => [
340 'forgot' => 1,
341 ],
342 ],
343 'all',
344 '&special1=23&special2[foo]=bar',
345 ],
346 'preserve single parameter' => [
347 [
348 'L' => 42,
349 ],
350 'L',
351 '&L=42'
352 ],
353 'preserve whole parameter array' => [
354 [
355 'L' => 3,
356 'tx_someext' => [
357 'foo' => 'simple',
358 'bar' => [
359 'baz' => 'simple',
360 ],
361 ],
362 ],
363 'L,tx_someext',
364 '&L=3&tx_someext[foo]=simple&tx_someext[bar][baz]=simple',
365 ],
366 'preserve part of sub array' => [
367 [
368 'L' => 3,
369 'tx_someext' => [
370 'foo' => 'simple',
371 'bar' => [
372 'baz' => 'simple',
373 ],
374 ],
375 ],
376 'L,tx_someext[bar]',
377 '&L=3&tx_someext[bar][baz]=simple',
378 ],
379 'preserve keys on different levels' => [
380 [
381 'L' => 3,
382 'no-preserve' => 'whatever',
383 'tx_ext2' => [
384 'foo' => 'simple',
385 ],
386 'tx_ext3' => [
387 'bar' => [
388 'baz' => 'simple',
389 ],
390 'go-away' => '',
391 ],
392 ],
393 'L,tx_ext2,tx_ext3[bar]',
394 '&L=3&tx_ext2[foo]=simple&tx_ext3[bar][baz]=simple',
395 ],
396 'preserved value that does not exist in get' => [
397 [],
398 'L,foo[bar]',
399 ''
400 ],
401 'url params are encoded' => [
402 ['tx_ext1' => 'param with spaces and \\ %<>& /'],
403 'L,tx_ext1',
404 '&tx_ext1=param%20with%20spaces%20and%20%5C%20%25%3C%3E%26%20%2F'
405 ],
406 ];
407 }
408
409 /**
410 * @test
411 * @dataProvider getPreserveGetVarsReturnsCorrectResultDataProvider
412 * @param array $getArray
413 * @param string $preserveVars
414 * @param string $expected
415 */
416 public function getPreserveGetVarsReturnsCorrectResult(array $getArray, $preserveVars, $expected)
417 {
418 $_GET = $getArray;
419 $this->accessibleFixture->conf['preserveGETvars'] = $preserveVars;
420 $this->assertSame($expected, $this->accessibleFixture->_call('getPreserveGetVars'));
421 }
422
423 /**************************************************
424 * Tests concerning isInLocalDomain
425 **************************************************/
426
427 /**
428 * Dataprovider for isInCurrentDomainIgnoresScheme
429 *
430 * @return array
431 */
432 public function isInCurrentDomainIgnoresSchemeDataProvider()
433 {
434 return [
435 'url https, current host http' => [
436 'example.com', // HTTP_HOST
437 '0', // HTTPS
438 'https://example.com/foo.html' // URL
439 ],
440 'url http, current host https' => [
441 'example.com',
442 '1',
443 'http://example.com/foo.html'
444 ],
445 'url https, current host https' => [
446 'example.com',
447 '1',
448 'https://example.com/foo.html'
449 ],
450 'url http, current host http' => [
451 'example.com',
452 '0',
453 'http://example.com/foo.html'
454 ]
455 ];
456 }
457
458 /**
459 * @test
460 * @dataProvider isInCurrentDomainIgnoresSchemeDataProvider
461 * @param string $host $_SERVER['HTTP_HOST']
462 * @param string $https $_SERVER['HTTPS']
463 * @param string $url The url to test
464 */
465 public function isInCurrentDomainIgnoresScheme($host, $https, $url)
466 {
467 $_SERVER['HTTP_HOST'] = $host;
468 $_SERVER['HTTPS'] = $https;
469 $this->assertTrue($this->accessibleFixture->_call('isInCurrentDomain', $url));
470 }
471
472 /**
473 * @return array
474 */
475 public function isInCurrentDomainReturnsFalseIfDomainsAreDifferentDataProvider()
476 {
477 return [
478 'simple difference' => [
479 'example.com', // HTTP_HOST
480 'http://typo3.org/foo.html' // URL
481 ],
482 'subdomain different' => [
483 'example.com',
484 'http://foo.example.com/bar.html'
485 ]
486 ];
487 }
488
489 /**
490 * @test
491 * @dataProvider isInCurrentDomainReturnsFalseIfDomainsAreDifferentDataProvider
492 * @param string $host $_SERVER['HTTP_HOST']
493 * @param string $url The url to test
494 */
495 public function isInCurrentDomainReturnsFalseIfDomainsAreDifferent($host, $url)
496 {
497 $_SERVER['HTTP_HOST'] = $host;
498 $this->assertFalse($this->accessibleFixture->_call('isInCurrentDomain', $url));
499 }
500
501 /**
502 * @test
503 */
504 public function processRedirectReferrerDomainsMatchesDomains()
505 {
506 $conf = [
507 'redirectMode' => 'refererDomains',
508 'domains' => 'example.com'
509 ];
510
511 $this->accessibleFixture->_set('conf', $conf);
512 $this->accessibleFixture->_set('logintype', LoginType::LOGIN);
513 $this->accessibleFixture->_set('referer', 'http://www.example.com/snafu');
514 /** @var TypoScriptFrontendController $tsfe */
515 $tsfe = $this->accessibleFixture->_get('frontendController');
516 $this->accessibleFixture->_set('userIsLoggedIn', true);
517 $this->assertSame(['http://www.example.com/snafu'], $this->accessibleFixture->_call('processRedirect'));
518 }
519 }