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