‪TYPO3CMS  9.5
TypoLinkGeneratorTest.php
Go to the documentation of this file.
1 <?php
2 declare(strict_types = 1);
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 
26 use TYPO3\TestingFramework\Core\Functional\Framework\DataHandling\Scenario\DataHandlerFactory;
27 use TYPO3\TestingFramework\Core\Functional\Framework\DataHandling\Scenario\DataHandlerWriter;
28 use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\Internal\AbstractInstruction;
29 use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\Internal\ArrayValueInstruction;
30 use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\Internal\TypoScriptInstruction;
31 use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalRequest;
32 use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalRequestContext;
33 use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalResponse;
34 
39 {
43  private ‪$siteTitle = 'A Company that Manufactures Everything Inc';
44 
49 
51  'typo3/sysext/backend/Resources/Public/Images/Logo.png' => 'fileadmin/logo.png'
52  ];
53 
54  public static function ‪setUpBeforeClass()
55  {
56  parent::setUpBeforeClass();
57  static::initializeDatabaseSnapshot();
58  }
59 
60  public static function ‪tearDownAfterClass()
61  {
62  static::destroyDatabaseSnapshot();
63  parent::tearDownAfterClass();
64  }
65 
66  protected function ‪setUp()
67  {
68  parent::setUp();
69 
70  // these settings are forwarded to the frontend sub-request as well
71  $this->internalRequestContext = (new InternalRequestContext())
72  ->withGlobalSettings(['TYPO3_CONF_VARS' => static::TYPO3_CONF_VARS]);
73 
75  'acme-com',
76  $this->‪buildSiteConfiguration(1000, 'https://acme.com/'),
77  [
78  $this->‪buildDefaultLanguageConfiguration('EN', 'https://acme.us/'),
79  $this->‪buildLanguageConfiguration('FR', 'https://acme.fr/', ['EN']),
80  $this->‪buildLanguageConfiguration('FR-CA', 'https://acme.ca/', ['FR', 'EN']),
81  ]
82  );
83 
84  $this->withDatabaseSnapshot(function () {
85  $this->‪setUpDatabase();
86  });
87  }
88 
89  protected function ‪setUpDatabase()
90  {
91  $backendUser = $this->setUpBackendUserFromFixture(1);
93 
94  $scenarioFile = __DIR__ . '/Fixtures/TypoLinkScenario.yaml';
95  $factory = DataHandlerFactory::fromYamlFile($scenarioFile);
96  $writer = DataHandlerWriter::withBackendUser($backendUser);
97  $writer->invokeFactory($factory);
98  static::failIfArrayIsNotEmpty(
99  $writer->getErrors()
100  );
101 
102  // @todo Provide functionality of assigning TSconfig to Testing Framework
103  $connection = GeneralUtility::makeInstance(ConnectionPool::class)
104  ->getConnectionForTable('pages');
106  $connection->update(
107  'pages',
108  ['TSconfig' => implode(chr(10), [
109  'TCEMAIN.linkHandler.content {',
110  ' configuration.table = tt_content',
111  '}',
112  ])],
113  ['uid' => 1000]
114  );
115 
116  $this->‪setUpFileStorage();
117  $this->setUpFrontendRootPage(
118  1000,
119  [
120  'typo3/sysext/frontend/Tests/Functional/SiteHandling/Fixtures/LinkGenerator.typoscript',
121  ],
122  [
123  'title' => 'ACME Root',
124  'sitetitle' => $this->siteTitle,
125  ]
126  );
127  }
128 
132  private function ‪setUpFileStorage()
133  {
134  $storageRepository = new StorageRepository();
135  $storageId = $storageRepository->createLocalStorage(
136  'fileadmin/ (auto-created)',
137  'fileadmin/',
138  'relative',
139  'Default storage created in TypoLinkTest',
140  true
141  );
142  $storage = $storageRepository->findByUid($storageId);
143  (new Indexer($storage))->processChangesInStorages();
144  }
145 
146  protected function ‪tearDown()
147  {
148  unset($this->internalRequestContext);
149  parent::tearDown();
150  }
151 
155  public function ‪linkIsGeneratedDataProvider(): array
156  {
157  $instructions = [
158  [
159  't3://email?email=mailto:user@example.org&other=other#other',
160  '<a href="mailto:user@example.org">user@example.org</a>',
161  ],
162  [
163  't3://email?email=user@example.org&other=other#other',
164  '<a href="mailto:user@example.org">user@example.org</a>',
165  ],
166  [
167  't3://email?email=user@example.org?subject=Hello%20World#other',
168  '<a href="mailto:user@example.org?subject=Hello World">user@example.org?subject=Hello World</a>',
169  ],
170  [
171  't3://file?uid=1&type=1&other=other#other',
172  '<a href="/fileadmin/logo.png#other">fileadmin/logo.png</a>',
173  ],
174  [
175  't3://file?identifier=1:/logo.png&other=other#other',
176  '<a href="/fileadmin/logo.png#other">fileadmin/logo.png</a>',
177  ],
178  [
179  't3://file?identifier=fileadmin/logo.png&other=other#other',
180  '<a href="/fileadmin/logo.png#other">fileadmin/logo.png</a>',
181  ],
182  [
183  't3://folder?identifier=fileadmin&other=other#other',
184  '<a href="/fileadmin/#other">fileadmin/</a>',
185  ],
186  [
187  't3://page?uid=1200&type=1&param-a=a&param-b=b#fragment',
188  '<a href="/features?param-a=a&amp;param-b=b&amp;type=1&amp;cHash=92aa5284d0ad18f7934fe94b52f6c1a5#fragment">EN: Features</a>',
189  ],
190  [
191  't3://record?identifier=content&uid=10001&other=other#fragment',
192  '<a href="/features#c10001">EN: Features</a>',
193  ],
194  [
195  't3://url?url=https://typo3.org%3f%26param-a=a%26param-b=b&other=other#other',
196  '<a href="https://typo3.org?&amp;param-a=a&amp;param-b=b">https://typo3.org?&amp;param-a=a&amp;param-b=b</a>',
197  ],
198  [
199  '1200,1 target class title &param-a=a',
200  '<a href="/features?param-a=a&amp;type=1&amp;cHash=62ac35c73f425af5e13cfff14c04424e" title="title" target="target" class="class">EN: Features</a>'
201  ],
202  [
203  'user@example.org target class title &other=other',
204  '<a href="mailto:user@example.org" title="title" target="target" class="class">user@example.org</a>'
205  ],
206  ];
207  return $this->keysFromTemplate($instructions, '%1$s;');
208  }
209 
217  public function ‪linkIsGenerated(string $parameter, string $expectation)
218  {
219  $response = $this->‪invokeTypoLink($parameter);
220  self::assertSame($expectation, (string)$response->getBody());
221  }
222 
226  public function ‪linkIsEncodedDataProvider(): array
227  {
228  $instructions = [
229  [
230  't3://email?email=mailto:<bad>thing(1)</bad>',
231  '<a href="mailto:&lt;bad&gt;thing(1)&lt;/bad&gt;">&lt;bad&gt;thing(1)&lt;/bad&gt;</a>',
232  ],
233  [
234  't3://email?email=mailto:<good%20a="a/"%20b="thing(1)">',
235  '<a href="mailto:&lt;good a=&quot;a/&quot; b=&quot;thing(1)&quot;&gt;">&lt;good a=&quot;a/&quot; b=&quot;thing(1)&quot;&gt;</a>',
236  ],
237  [
238  't3://email?email=<bad>thing(1)</bad>',
239  '<a href="mailto:&lt;bad&gt;thing(1)&lt;/bad&gt;">&lt;bad&gt;thing(1)&lt;/bad&gt;</a>',
240  ],
241  [
242  't3://email?email=<good%20a="a/"%20b="thing(1)">',
243  '<a href="mailto:&lt;good a=&quot;a/&quot; b=&quot;thing(1)&quot;&gt;">&lt;good a=&quot;a/&quot; b=&quot;thing(1)&quot;&gt;</a>',
244  ],
245  [
246  't3://folder?identifier=<any>',
247  '',
248  ],
249  [
250  't3://page?uid=<any>',
251  '',
252  ],
253  [
254  't3://record?identifier=content&uid=<any>',
255  '',
256  ],
257  [
258  't3://url?url=<bad>thing(1)</bad>',
259  '<a href="http://&lt;bad&gt;thing(1)&lt;/bad&gt;">http://&lt;bad&gt;thing(1)&lt;/bad&gt;</a>'
260  ],
261  [
262  't3://url?url=<good%20a="a/"%20b="thing(1)">',
263  '<a href="http://&lt;good a=&quot;a/&quot; b=&quot;thing(1)&quot;&gt;">http://&lt;good a=&quot;a/&quot; b=&quot;thing(1)&quot;&gt;</a>'
264  ],
265  [
266  't3://url?url=javascript:good()',
267  '<a ></a>'
268  ],
269  [
270  "t3://url?url=java\tscript:good()",
271  '<a href="http://java_script:good()">http://java_script:good()</a>'
272  ],
273  [
274  't3://url?url=java&#09;script:good()',
275  '<a href="http://java">http://java</a>'
276  ],
277  [
278  't3://url?url=javascript&colon;good()',
279  '<a href="http://javascript">http://javascript</a>'
280  ],
281  [
282  't3://url?url=data:text/html,<good>',
283  '<a ></a>'
284  ],
285  [
286  "t3://url?url=da\tsta:text/html,<good>",
287  '<a href="http://da_sta:text/html,&lt;good&gt;">http://da_sta:text/html,&lt;good&gt;</a>'
288  ],
289  [
290  't3://url?url=da&#09;ta:text/html,<good>',
291  '<a href="http://da">http://da</a>'
292  ],
293  [
294  't3://url?url=data&colon;text/html,<good>',
295  '<a href="http://data">http://data</a>'
296  ],
297  [
298  't3://url?url=%26%23106%3B%26%2397%3B%26%23118%3B%26%2397%3B%26%23115%3B%26%2399%3B%26%23114%3B%26%23105%3B%26%23112%3B%26%23116%3B%26%2358%3B%26%23103%3B%26%23111%3B%26%23111%3B%26%23100%3B%26%2340%3B%26%2341%3B',
299  '<a href="http://&amp;#106;&amp;#97;&amp;#118;&amp;#97;&amp;#115;&amp;#99;&amp;#114;&amp;#105;&amp;#112;&amp;#116;&amp;#58;&amp;#103;&amp;#111;&amp;#111;&amp;#100;&amp;#40;&amp;#41;">http://&amp;#106;&amp;#97;&amp;#118;&amp;#97;&amp;#115;&amp;#99;&amp;#114;&amp;#105;&amp;#112;&amp;#116;&amp;#58;&amp;#103;&amp;#111;&amp;#111;&amp;#100;&amp;#40;&amp;#41;</a>',
300  ],
301  [
302  '<bad>thing(1)</bad>',
303  '<a href="/&lt;bad&gt;thing(1)&lt;/bad&gt;">&lt;bad&gt;thing(1)&lt;/bad&gt;</a>'
304  ],
305  [
306  '<good%20a="a/"%20b="thing(1)">',
307  '<a href="/&lt;good%20a=&quot;a/&quot;%20b=&quot;thing(1)&quot;&gt;">&lt;good a=&quot;a/&quot; b=&quot;thing(1)&quot;&gt;</a>'
308  ],
309  [
310  '<good/a="a/"/b="thing(1)"> target class title &other=other',
311  '<a href="/&lt;good/a=&quot;a/&quot;/b=&quot;thing(1)&quot;&gt;" title="title" target="target" class="class">&lt;good/a=&quot;a/&quot;/b=&quot;thing(1)&quot;&gt;</a>'
312  ],
313  [
314  'javascript:good()',
315  '',
316  ],
317  [
318  "java\tscript:good()",
319  '',
320  ],
321  [
322  'java&#09;script:good()',
323  '<a href="java&amp;#09;script:good()"></a>'
324  ],
325  [
326  'javascript&colon;good()',
327  ''
328  ],
329  [
330  'data:text/html,<good>',
331  '',
332  ],
333  [
334  "da\tta:text/html,<good>",
335  '',
336  ],
337  [
338  'da&#09;ta:text/html,<good>',
339  '<a href="da&amp;#09;ta:text/html,&lt;good&gt;"></a>',
340  ],
341  [
342  'data&colon;text/html,<good>',
343  '<a href="/data&amp;colon;text/html,&lt;good&gt;">data&amp;colon;text/html,&lt;good&gt;</a>',
344  ],
345  [
346  '%26%23106%3B%26%2397%3B%26%23118%3B%26%2397%3B%26%23115%3B%26%2399%3B%26%23114%3B%26%23105%3B%26%23112%3B%26%23116%3B%26%2358%3B%26%23103%3B%26%23111%3B%26%23111%3B%26%23100%3B%26%2340%3B%26%2341%3B',
347  '',
348  ],
349  [
350  '</> <"> <"> <">',
351  '<a href="/&lt;/&gt;" title="&lt;&quot;&gt;" target="&lt;&quot;&gt;" class="&lt;&quot;&gt;">&lt;/&gt;</a>',
352  ],
353  ];
354  return $this->keysFromTemplate($instructions, '%1$s;');
355  }
356 
364  public function ‪linkIsEncodedPerDefault(string $parameter, string $expectation)
365  {
366  $response = $this->‪invokeTypoLink($parameter);
367  self::assertSame($expectation, (string)$response->getBody());
368  }
369 
377  public function ‪linkIsEncodedHavingParseFunc(string $parameter, string $expectation)
378  {
379  $response = $this->‪invokeTypoLink($parameter, $this->‪createParseFuncInstruction([
380  'allowTags' => 'good',
381  'denyTags' => '*',
382  'nonTypoTagStdWrap.' => [
383  'HTMLparser.' => [
384  'tags.' => [
385  'good.' => [
386  'allowedAttribs' => 0,
387  ],
388  ],
389  ],
390  ],
391  ]));
392  self::assertSame($expectation, (string)$response->getBody());
393  }
394 
398  public function ‪linkToPageIsProcessedDataProvider(): array
399  {
400  return [
401  [
402  't3://page?uid=9911',
403  '<a href="/test/good">&lt;good&gt;</a>',
404  false,
405  ],
406  [
407  't3://page?uid=9911',
408  '<a href="/test/good"><good></good></a>', // expanded from `<good>` to `<good></good>`
409  true,
410  ],
411  [
412  't3://page?uid=9912',
413  '<a href="/test/good-a-b-spaced">&lt;good a=&quot;a/&quot; b=&quot;thing(1)&quot;&gt;</a>',
414  false,
415  ],
416  [
417  't3://page?uid=9912',
418  '<a href="/test/good-a-b-spaced"><good></good></a>', // expanded from `<good>` to `<good></good>`
419  true,
420  ],
421  [
422  't3://page?uid=9913',
423  '<a href="/test/good-a-b-enc-a">&lt;good%20a=&quot;a/&quot;%20b=&quot;thing(1)&quot;&gt;</a>',
424  false,
425  ],
426  [
427  't3://page?uid=9913',
428  '<a href="/test/good-a-b-enc-a">&lt;good%20a="a/"%20b="thing(1)"&gt;</a>',
429  true,
430  ],
431  [
432  't3://page?uid=9914',
433  '<a href="/test/good-a-b-enc-b">&lt;good/a=&quot;a/&quot;/b=&quot;thing(1)&quot;&gt;</a>',
434  false,
435  ],
436  [
437  't3://page?uid=9914',
438  '<a href="/test/good-a-b-enc-b">&lt;good/a="a/"/b="thing(1)"&gt;</a>',
439  true,
440  ],
441  [
442  't3://page?uid=9921',
443  '<a href="/test/bad">&lt;bad&gt;</a>',
444  false,
445  ],
446  [
447  't3://page?uid=9921',
448  '<a href="/test/bad">&lt;bad&gt;</a>',
449  true,
450  ],
451  ];
452  }
453 
462  public function ‪linkToPageIsProcessed(string $parameter, string $expectation, bool $parseFuncEnabled)
463  {
464  $instructions = [];
465  if ($parseFuncEnabled) {
466  $instructions[] = $this->‪createParseFuncInstruction([
467  'allowTags' => 'good',
468  'denyTags' => '*',
469  'nonTypoTagStdWrap.' => [
470  'HTMLparser.' => [
471  'tags.' => [
472  'good.' => [
473  'allowedAttribs' => 0,
474  ],
475  ],
476  ],
477  ],
478  'htmlSanitize' => true,
479  'htmlSanitize.' => [
480  'build' => TestSanitizerBuilder::class,
481  ],
482  ]);
483  }
484  $response = $this->‪invokeTypoLink($parameter, ...$instructions);
485  self::assertSame($expectation, (string)$response->getBody());
486  }
487 
493  private function ‪invokeTypoLink(string $parameter, AbstractInstruction ...$instructions): InternalResponse
494  {
495  $sourcePageId = 1100;
496  $targetPageId = 1200;
497 
498  $request = (new InternalRequest('https://acme.us/'))
499  ->withPageId($sourcePageId)
500  ->withInstructions(
501  [
503  'parameter' => $targetPageId,
504  'section.' => [
505  'data' => 'field:uid',
506  'wrap' => 'c|',
507  ],
508  ]),
510  'parameter' => $parameter,
511  ])
512  ]
513  );
514 
515  if (count($instructions) > 0) {
516  $request = $this->‪applyInstructions($request, ...$instructions);
517  }
518 
519  return $this->executeFrontendRequest($request, $this->internalRequestContext);
520  }
521 
526  private function ‪createTypoLinkInstruction(array $typoLink): ArrayValueInstruction
527  {
528  return (new ArrayValueInstruction(LinkHandlingController::class))
529  ->withArray([
530  '10' => 'TEXT',
531  '10.' => [
532  'typolink.' => $typoLink
533  ]
534  ]);
535  }
536 
541  private function ‪createRecordLinksInstruction(array $typoLink): TypoScriptInstruction
542  {
543  return (new TypoScriptInstruction(TemplateService::class))
544  ->withTypoScript([
545  'config.' => [
546  'recordLinks.' => [
547  'content.' => [
548  'forceLink' => 1,
549  'typolink.' => $typoLink,
550  ],
551  ],
552  ],
553  ]);
554  }
555 
560  private function ‪createParseFuncInstruction(array $parseFunc): TypoScriptInstruction
561  {
562  return (new TypoScriptInstruction(TemplateService::class))
563  ->withTypoScript([
564  'lib.' => [
565  'parseFunc.' => array_replace_recursive(
566  [
567  'makelinks' => 1,
568  'makelinks.' => [
569  'http.' => [
570  'keep' => 'path',
571  'extTarget' => '_blank',
572  ],
573  'mailto.' => [
574  'keep' => 'path',
575  ],
576  ],
577  'allowTags' => '',
578  'denyTags' => '',
579  'constants' => 1,
580  'nonTypoTagStdWrap.' => [
581  'HTMLparser' => 1,
582  'HTMLparser.' => [
583  'keepNonMatchedTags' => 1,
584  'htmlSpecialChars' => 2,
585  ],
586  ],
587  ],
588  $parseFunc
589  ),
590  ],
591  ]);
592  }
593 }
‪TYPO3\CMS\Core\Resource\Index\Indexer
Definition: Indexer.php:33
‪TYPO3\CMS\Core\Tests\Functional\SiteHandling\SiteBasedTestTrait\buildLanguageConfiguration
‪array buildLanguageConfiguration(string $identifier, string $base, array $fallbackIdentifiers=[], string $fallbackType=null)
Definition: SiteBasedTestTrait.php:140
‪TYPO3\CMS\Core\Tests\Functional\SiteHandling\SiteBasedTestTrait\writeSiteConfiguration
‪writeSiteConfiguration(string $identifier, array $site=[], array $languages=[], array $errorHandling=[])
Definition: SiteBasedTestTrait.php:56
‪TYPO3\CMS\Frontend\Tests\Functional\SiteHandling\AbstractTestCase
Definition: AbstractTestCase.php:29
‪TYPO3\CMS\Core\Tests\Functional\SiteHandling\SiteBasedTestTrait\buildDefaultLanguageConfiguration
‪array buildDefaultLanguageConfiguration(string $identifier, string $base)
Definition: SiteBasedTestTrait.php:122
‪TYPO3\CMS\Core\Resource\StorageRepository
Definition: StorageRepository.php:29
‪TYPO3\CMS\Frontend\Tests\Functional\SiteHandling\Fixtures\TestSanitizerBuilder
Definition: TestSanitizerBuilder.php:24
‪TYPO3\CMS\Core\TypoScript\TemplateService
Definition: TemplateService.php:50
‪TYPO3\CMS\Frontend\Tests\Functional\SiteHandling
Definition: AbstractTestCase.php:3
‪TYPO3\CMS\Core\Tests\Functional\SiteHandling\SiteBasedTestTrait\applyInstructions
‪InternalRequest applyInstructions(InternalRequest $request, AbstractInstruction ... $instructions)
Definition: SiteBasedTestTrait.php:241
‪TYPO3\CMS\Core\Core\Bootstrap
Definition: Bootstrap.php:50
‪TYPO3\CMS\Core\Tests\Functional\SiteHandling\SiteBasedTestTrait\buildSiteConfiguration
‪array buildSiteConfiguration(int $rootPageId, string $base='')
Definition: SiteBasedTestTrait.php:107
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:44
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:45
‪TYPO3\CMS\Core\Core\Bootstrap\initializeLanguageObject
‪static Bootstrap null initializeLanguageObject()
Definition: Bootstrap.php:986