‪TYPO3CMS  10.4
TypoLinkGeneratorTest.php
Go to the documentation of this file.
1 <?php
2 
3 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 
19 
28 use TYPO3\TestingFramework\Core\Functional\Framework\DataHandling\Scenario\DataHandlerFactory;
29 use TYPO3\TestingFramework\Core\Functional\Framework\DataHandling\Scenario\DataHandlerWriter;
30 use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\Internal\AbstractInstruction;
31 use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\Internal\ArrayValueInstruction;
32 use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\Internal\TypoScriptInstruction;
33 use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalRequest;
34 use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalRequestContext;
35 use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalResponse;
36 
41 {
45  private ‪$siteTitle = 'A Company that Manufactures Everything Inc';
46 
51 
53  'typo3/sysext/backend/Resources/Public/Images/Logo.png' => 'fileadmin/logo.png'
54  ];
55 
56  public static function ‪setUpBeforeClass(): void
57  {
58  parent::setUpBeforeClass();
59  static::initializeDatabaseSnapshot();
60  }
61 
62  public static function ‪tearDownAfterClass(): void
63  {
64  static::destroyDatabaseSnapshot();
65  parent::tearDownAfterClass();
66  }
67 
68  protected function ‪setUp(): void
69  {
70  parent::setUp();
71 
72  // these settings are forwarded to the frontend sub-request as well
73  $this->internalRequestContext = (new InternalRequestContext())
74  ->withGlobalSettings(['TYPO3_CONF_VARS' => static::TYPO3_CONF_VARS]);
75 
77  'acme-com',
78  $this->‪buildSiteConfiguration(1000, 'https://acme.com/'),
79  [
80  $this->‪buildDefaultLanguageConfiguration('EN', 'https://acme.us/'),
81  $this->‪buildLanguageConfiguration('FR', 'https://acme.fr/', ['EN']),
82  $this->‪buildLanguageConfiguration('FR-CA', 'https://acme.ca/', ['FR', 'EN']),
83  ]
84  );
85 
86  $this->withDatabaseSnapshot(function () {
87  $this->‪setUpDatabase();
88  });
89  }
90 
91  protected function ‪setUpDatabase()
92  {
93  $backendUser = $this->setUpBackendUserFromFixture(1);
95 
96  $scenarioFile = __DIR__ . '/Fixtures/TypoLinkScenario.yaml';
97  $factory = DataHandlerFactory::fromYamlFile($scenarioFile);
98  $writer = DataHandlerWriter::withBackendUser($backendUser);
99  $writer->invokeFactory($factory);
100  static::failIfArrayIsNotEmpty(
101  $writer->getErrors()
102  );
103 
104  // @todo Provide functionality of assigning TSconfig to Testing Framework
105  $connection = GeneralUtility::makeInstance(ConnectionPool::class)
106  ->getConnectionForTable('pages');
108  $connection->update(
109  'pages',
110  ['TSconfig' => implode(chr(10), [
111  'TCEMAIN.linkHandler.content {',
112  ' configuration.table = tt_content',
113  '}',
114  ])],
115  ['uid' => 1000]
116  );
117 
118  $this->‪setUpFileStorage();
119  $this->setUpFrontendRootPage(
120  1000,
121  [
122  'typo3/sysext/frontend/Tests/Functional/SiteHandling/Fixtures/LinkGenerator.typoscript',
123  ],
124  [
125  'title' => 'ACME Root',
126  'sitetitle' => $this->siteTitle,
127  ]
128  );
129  }
130 
134  private function ‪setUpFileStorage()
135  {
136  $storageRepository = new StorageRepository();
137  $storageId = $storageRepository->createLocalStorage(
138  'fileadmin/ (auto-created)',
139  'fileadmin/',
140  'relative',
141  'Default storage created in TypoLinkTest',
142  true
143  );
144  $storage = $storageRepository->findByUid($storageId);
145  (new Indexer($storage))->processChangesInStorages();
146  }
147 
148  protected function ‪tearDown(): void
149  {
150  unset($this->internalRequestContext);
151  parent::tearDown();
152  }
153 
157  public function ‪linkIsGeneratedDataProvider(): array
158  {
159  $instructions = [
160  [
161  't3://email?email=mailto:user@example.org&other=other#other',
162  '<a href="mailto:user@example.org">user@example.org</a>',
163  ],
164  [
165  't3://email?email=user@example.org&other=other#other',
166  '<a href="mailto:user@example.org">user@example.org</a>',
167  ],
168  [
169  't3://email?email=user@example.org?subject=Hello%20World#other',
170  '<a href="mailto:user@example.org?subject=Hello World">user@example.org?subject=Hello World</a>',
171  ],
172  [
173  't3://file?uid=1&type=1&other=other#other',
174  '<a href="/fileadmin/logo.png#other">fileadmin/logo.png</a>',
175  ],
176  [
177  't3://file?identifier=1:/logo.png&other=other#other',
178  '<a href="/fileadmin/logo.png#other">fileadmin/logo.png</a>',
179  ],
180  [
181  't3://file?identifier=fileadmin/logo.png&other=other#other',
182  '<a href="/fileadmin/logo.png#other">fileadmin/logo.png</a>',
183  ],
184  [
185  't3://folder?identifier=fileadmin&other=other#other',
186  '<a href="/fileadmin/#other">fileadmin/</a>',
187  ],
188  [
189  't3://page?uid=1200&type=1&param-a=a&param-b=b#fragment',
190  '<a href="/features?param-a=a&amp;param-b=b&amp;type=1&amp;cHash=92aa5284d0ad18f7934fe94b52f6c1a5#fragment">EN: Features</a>',
191  ],
192  [
193  't3://page?uid=1300&additional=1&param-a=a#fragment',
194  '<a href="http://typo3.org">Go to TYPO3.org</a>',
195  ],
196  [
197  't3://record?identifier=content&uid=10001&other=other#fragment',
198  '<a href="/features#c10001">EN: Features</a>',
199  ],
200  [
201  't3://url?url=https://typo3.org%3f%26param-a=a%26param-b=b&other=other#other',
202  '<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>',
203  ],
204  [
205  '1200,1 target class title &param-a=a',
206  '<a href="/features?param-a=a&amp;type=1&amp;cHash=62ac35c73f425af5e13cfff14c04424e" title="title" target="target" class="class">EN: Features</a>'
207  ],
208  [
209  'user@example.org target class title &other=other',
210  '<a href="mailto:user@example.org" title="title" target="target" class="class">user@example.org</a>'
211  ],
212  // check link with language parameters
213  [
214  't3://page?uid=1200&L=0',
215  '<a href="/features">EN: Features</a>',
216  ],
217  [
218  't3://page?uid=1200&_language=0',
219  '<a href="/features">EN: Features</a>',
220  ],
221  [
222  't3://page?uid=1200&L=1',
223  '<a href="https://acme.fr/features-fr">FR: Features</a>',
224  ],
225  [
226  't3://page?uid=1200&_language=1',
227  '<a href="https://acme.fr/features-fr">FR: Features</a>',
228  ],
229  [
230  't3://page?uid=1201&L=1',
231  '<a href="https://acme.fr/features-fr">FR: Features</a>',
232  ],
233  [
234  't3://page?uid=1201&_language=1',
235  '<a href="https://acme.fr/features-fr">FR: Features</a>',
236  ],
237  // localized page language overrule language arguments (new and old).
238  // This has also test coverage through SlugGeneratorTests.
239  [
240  't3://page?uid=1202&L=1',
241  '<a href="https://acme.ca/features-ca">FR-CA: Features</a>',
242  ],
243  [
244  't3://page?uid=1202&_language=1',
245  '<a href="https://acme.ca/features-ca">FR-CA: Features</a>',
246  ],
247  // check precedence order correctness if old and modern are provided
248  [
249  't3://page?uid=1200&L=2&_language=1',
250  '<a href="https://acme.fr/features-fr">FR: Features</a>',
251  ],
252  [
253  't3://page?uid=1200&_language=1&L=2',
254  '<a href="https://acme.fr/features-fr">FR: Features</a>',
255  ],
256  [
257  't3://page?uid=1200&L=1&_language=2',
258  '<a href="https://acme.ca/features-ca">FR-CA: Features</a>',
259  ],
260  [
261  't3://page?uid=1200&_language=2&L=1',
262  '<a href="https://acme.ca/features-ca">FR-CA: Features</a>',
263  ],
264  ];
265  return $this->keysFromTemplate($instructions, '%1$s;');
266  }
267 
275  public function ‪linkIsGenerated(string $parameter, string $expectation)
276  {
277  $response = $this->‪invokeTypoLink($parameter);
278  self::assertSame($expectation, (string)$response->getBody());
279  }
280 
284  public function ‪linkIsEncodedDataProvider(): array
285  {
286  $instructions = [
287  [
288  't3://email?email=mailto:<bad>thing(1)</bad>',
289  '<a href="mailto:&lt;bad&gt;thing(1)&lt;/bad&gt;">&lt;bad&gt;thing(1)&lt;/bad&gt;</a>',
290  ],
291  [
292  't3://email?email=mailto:<good%20a="a/"%20b="thing(1)">',
293  '<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>',
294  ],
295  [
296  't3://email?email=<bad>thing(1)</bad>',
297  '<a href="mailto:&lt;bad&gt;thing(1)&lt;/bad&gt;">&lt;bad&gt;thing(1)&lt;/bad&gt;</a>',
298  ],
299  [
300  't3://email?email=<good%20a="a/"%20b="thing(1)">',
301  '<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>',
302  ],
303  [
304  't3://folder?identifier=<any>',
305  '',
306  ],
307  [
308  't3://page?uid=<any>',
309  '',
310  ],
311  [
312  't3://record?identifier=content&uid=<any>',
313  '',
314  ],
315  [
316  't3://url?url=<bad>thing(1)</bad>',
317  '<a href="http://&lt;bad&gt;thing(1)&lt;/bad&gt;">http://&lt;bad&gt;thing(1)&lt;/bad&gt;</a>'
318  ],
319  [
320  't3://url?url=<good%20a="a/"%20b="thing(1)">',
321  '<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>'
322  ],
323  [
324  't3://url?url=javascript:good()',
325  '<a ></a>'
326  ],
327  [
328  "t3://url?url=java\tscript:good()",
329  '<a href="http://java_script:good()">http://java_script:good()</a>'
330  ],
331  [
332  't3://url?url=java&#09;script:good()',
333  '<a href="http://java">http://java</a>'
334  ],
335  [
336  't3://url?url=javascript&colon;good()',
337  '<a href="http://javascript">http://javascript</a>'
338  ],
339  [
340  't3://url?url=data:text/html,<good>',
341  '<a ></a>'
342  ],
343  [
344  "t3://url?url=da\tsta:text/html,<good>",
345  '<a href="http://da_sta:text/html,&lt;good&gt;">http://da_sta:text/html,&lt;good&gt;</a>'
346  ],
347  [
348  't3://url?url=da&#09;ta:text/html,<good>',
349  '<a href="http://da">http://da</a>'
350  ],
351  [
352  't3://url?url=data&colon;text/html,<good>',
353  '<a href="http://data">http://data</a>'
354  ],
355  [
356  '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',
357  '<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>',
358  ],
359  [
360  '<bad>thing(1)</bad>',
361  '<a href="/&lt;bad&gt;thing(1)&lt;/bad&gt;">&lt;bad&gt;thing(1)&lt;/bad&gt;</a>'
362  ],
363  [
364  '<good%20a="a/"%20b="thing(1)">',
365  '<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>'
366  ],
367  [
368  '<good/a="a/"/b="thing(1)"> target class title &other=other',
369  '<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>'
370  ],
371  [
372  'javascript:good()',
373  '',
374  ],
375  [
376  "java\tscript:good()",
377  '',
378  ],
379  [
380  'java&#09;script:good()',
381  '<a href="java&amp;#09;script:good()"></a>'
382  ],
383  [
384  'javascript&colon;good()',
385  ''
386  ],
387  [
388  'data:text/html,<good>',
389  '',
390  ],
391  [
392  "da\tta:text/html,<good>",
393  '',
394  ],
395  [
396  'da&#09;ta:text/html,<good>',
397  '<a href="da&amp;#09;ta:text/html,&lt;good&gt;"></a>',
398  ],
399  [
400  'data&colon;text/html,<good>',
401  '<a href="/data&amp;colon;text/html,&lt;good&gt;">data&amp;colon;text/html,&lt;good&gt;</a>',
402  ],
403  [
404  '%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',
405  '',
406  ],
407  [
408  '</> <"> <"> <">',
409  '<a href="/&lt;/&gt;" title="&lt;&quot;&gt;" target="&lt;&quot;&gt;" class="&lt;&quot;&gt;">&lt;/&gt;</a>',
410  ],
411  ];
412  return $this->keysFromTemplate($instructions, '%1$s;');
413  }
414 
422  public function ‪linkIsEncodedPerDefault(string $parameter, string $expectation)
423  {
424  $response = $this->‪invokeTypoLink($parameter);
425  self::assertSame($expectation, (string)$response->getBody());
426  }
427 
435  public function ‪linkIsEncodedHavingParseFunc(string $parameter, string $expectation)
436  {
437  $response = $this->‪invokeTypoLink($parameter, $this->‪createParseFuncInstruction([
438  'allowTags' => 'good',
439  'denyTags' => '*',
440  'nonTypoTagStdWrap.' => [
441  'HTMLparser.' => [
442  'tags.' => [
443  'good.' => [
444  'allowedAttribs' => 0,
445  ],
446  ],
447  ],
448  ],
449  ]));
450  self::assertSame($expectation, (string)$response->getBody());
451  }
452 
456  public function ‪linkToPageIsProcessedDataProvider(): array
457  {
458  return [
459  [
460  't3://page?uid=9911',
461  '<a href="/test/good">&lt;good&gt;</a>',
462  false,
463  ],
464  [
465  't3://page?uid=9911',
466  '<a href="/test/good"><good></good></a>', // expanded from `<good>` to `<good></good>`
467  true,
468  ],
469  [
470  't3://page?uid=9912',
471  '<a href="/test/good-a-b-spaced">&lt;good a=&quot;a/&quot; b=&quot;thing(1)&quot;&gt;</a>',
472  false,
473  ],
474  [
475  't3://page?uid=9912',
476  '<a href="/test/good-a-b-spaced"><good></good></a>', // expanded from `<good>` to `<good></good>`
477  true,
478  ],
479  [
480  't3://page?uid=9913',
481  '<a href="/test/good-a-b-enc-a">&lt;good%20a=&quot;a/&quot;%20b=&quot;thing(1)&quot;&gt;</a>',
482  false,
483  ],
484  [
485  't3://page?uid=9913',
486  '<a href="/test/good-a-b-enc-a">&lt;good%20a="a/"%20b="thing(1)"&gt;</a>',
487  true,
488  ],
489  [
490  't3://page?uid=9914',
491  '<a href="/test/good-a-b-enc-b">&lt;good/a=&quot;a/&quot;/b=&quot;thing(1)&quot;&gt;</a>',
492  false,
493  ],
494  [
495  't3://page?uid=9914',
496  '<a href="/test/good-a-b-enc-b">&lt;good/a="a/"/b="thing(1)"&gt;</a>',
497  true,
498  ],
499  [
500  't3://page?uid=9921',
501  '<a href="/test/bad">&lt;bad&gt;</a>',
502  false,
503  ],
504  [
505  't3://page?uid=9921',
506  '<a href="/test/bad">&lt;bad&gt;</a>',
507  true,
508  ],
509  ];
510  }
511 
520  public function ‪linkToPageIsProcessed(string $parameter, string $expectation, bool $parseFuncEnabled)
521  {
522  $instructions = [];
523  if ($parseFuncEnabled) {
524  $instructions[] = $this->‪createParseFuncInstruction([
525  'allowTags' => 'good',
526  'denyTags' => '*',
527  'nonTypoTagStdWrap.' => [
528  'HTMLparser.' => [
529  'tags.' => [
530  'good.' => [
531  'allowedAttribs' => 0,
532  ],
533  ],
534  ],
535  ],
536  'htmlSanitize' => true,
537  'htmlSanitize.' => [
538  'build' => TestSanitizerBuilder::class,
539  ],
540  ]);
541  }
542  $response = $this->‪invokeTypoLink($parameter, ...$instructions);
543  self::assertSame($expectation, (string)$response->getBody());
544  }
545 
551  private function ‪invokeTypoLink(string $parameter, AbstractInstruction ...$instructions): InternalResponse
552  {
553  $sourcePageId = 1100;
554  $targetPageId = 1200;
555 
556  $request = (new InternalRequest('https://acme.us/'))
557  ->withPageId($sourcePageId)
558  ->withInstructions(
559  [
561  'parameter' => $targetPageId,
562  'section.' => [
563  'data' => 'field:uid',
564  'wrap' => 'c|',
565  ],
566  ]),
568  'parameter' => $parameter,
569  ])
570  ]
571  );
572 
573  if (count($instructions) > 0) {
574  $request = $this->‪applyInstructions($request, ...$instructions);
575  }
576 
577  return $this->executeFrontendRequest($request, $this->internalRequestContext);
578  }
579 
584  private function ‪createTypoLinkInstruction(array $typoLink): ArrayValueInstruction
585  {
586  return (new ArrayValueInstruction(LinkHandlingController::class))
587  ->withArray([
588  '10' => 'TEXT',
589  '10.' => [
590  'typolink.' => $typoLink
591  ]
592  ]);
593  }
594 
599  private function ‪createRecordLinksInstruction(array $typoLink): TypoScriptInstruction
600  {
601  return (new TypoScriptInstruction(TemplateService::class))
602  ->withTypoScript([
603  'config.' => [
604  'recordLinks.' => [
605  'content.' => [
606  'forceLink' => 1,
607  'typolink.' => $typoLink,
608  ],
609  ],
610  ],
611  ]);
612  }
613 
618  private function ‪createParseFuncInstruction(array $parseFunc): TypoScriptInstruction
619  {
620  return (new TypoScriptInstruction(TemplateService::class))
621  ->withTypoScript([
622  'lib.' => [
623  'parseFunc.' => array_replace_recursive(
624  [
625  'makelinks' => 1,
626  'makelinks.' => [
627  'http.' => [
628  'keep' => 'path',
629  'extTarget' => '_blank',
630  ],
631  'mailto.' => [
632  'keep' => 'path',
633  ],
634  ],
635  'allowTags' => '',
636  'denyTags' => '',
637  'constants' => 1,
638  'nonTypoTagStdWrap.' => [
639  'HTMLparser' => 1,
640  'HTMLparser.' => [
641  'keepNonMatchedTags' => 1,
642  'htmlSpecialChars' => 2,
643  ],
644  ],
645  ],
646  $parseFunc
647  ),
648  ],
649  ]);
650  }
651 }
‪TYPO3\CMS\Core\Resource\Index\Indexer
Definition: Indexer.php:34
‪TYPO3\CMS\Core\Tests\Functional\SiteHandling\SiteBasedTestTrait\buildLanguageConfiguration
‪array buildLanguageConfiguration(string $identifier, string $base, array $fallbackIdentifiers=[], string $fallbackType=null)
Definition: SiteBasedTestTrait.php:142
‪TYPO3\CMS\Core\Tests\Functional\SiteHandling\SiteBasedTestTrait\writeSiteConfiguration
‪writeSiteConfiguration(string $identifier, array $site=[], array $languages=[], array $errorHandling=[])
Definition: SiteBasedTestTrait.php:58
‪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:124
‪TYPO3\CMS\Core\Core\Bootstrap\initializeLanguageObject
‪static initializeLanguageObject()
Definition: Bootstrap.php:617
‪TYPO3\CMS\Core\Resource\StorageRepository
Definition: StorageRepository.php:31
‪TYPO3\CMS\Frontend\Tests\Functional\SiteHandling\Fixtures\TestSanitizerBuilder
Definition: TestSanitizerBuilder.php:24
‪TYPO3\CMS\Core\TypoScript\TemplateService
Definition: TemplateService.php:46
‪TYPO3\CMS\Frontend\Tests\Functional\SiteHandling
Definition: AbstractTestCase.php:18
‪TYPO3\CMS\Core\Tests\Functional\SiteHandling\SiteBasedTestTrait\applyInstructions
‪InternalRequest applyInstructions(InternalRequest $request, AbstractInstruction ... $instructions)
Definition: SiteBasedTestTrait.php:243
‪TYPO3\CMS\Core\Core\Bootstrap
Definition: Bootstrap.php:66
‪TYPO3\CMS\Core\Tests\Functional\SiteHandling\SiteBasedTestTrait\buildSiteConfiguration
‪array buildSiteConfiguration(int $rootPageId, string $base='')
Definition: SiteBasedTestTrait.php:109
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:46
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:46