‪TYPO3CMS  ‪main
ExternalLinktypeTest.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 
20 use GuzzleHttp\Cookie\CookieJar;
21 use GuzzleHttp\Exception\ClientException;
22 use GuzzleHttp\Psr7\Response;
23 use PHPUnit\Framework\Attributes\DataProvider;
24 use PHPUnit\Framework\Attributes\Test;
25 use PHPUnit\Framework\MockObject\MockObject;
31 use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
32 
33 final class ‪ExternalLinktypeTest extends UnitTestCase
34 {
35  protected function ‪setUp(): void
36  {
37  parent::setUp();
38  ‪$GLOBALS['LANG'] = $this->‪buildLanguageServiceMock();
39  }
40 
41  private function ‪buildLanguageServiceMock(): MockObject
42  {
43  $languageServiceMock = $this->getMockBuilder(LanguageService::class)->disableOriginalConstructor()->getMock();
44  return $languageServiceMock;
45  }
46 
47  #[Test]
49  {
50  $response = new Response(404);
51  $clientExceptionMock = $this->getMockBuilder(ClientException::class)->disableOriginalConstructor()->getMock();
52  $clientExceptionMock->expects(self::once())->method('hasResponse')->willReturn(true);
53  $clientExceptionMock->expects(self::once())->method('getResponse')->willReturn($response);
54 
55  ‪$url = 'https://example.org/~not-existing-url';
56  $options = $this->‪getRequestHeaderOptions();
57  $requestFactoryMock = $this->getMockBuilder(RequestFactory::class)->disableOriginalConstructor()->getMock();
58  $requestFactoryMock->method('request')->with(‪$url, 'HEAD', $options)
59  ->willThrowException($clientExceptionMock);
60 
61  $optionsSecondTryWithGET = array_merge_recursive($options, ['headers' => ['Range' => 'bytes=0-4048']]);
62  $requestFactoryMock->method('request')->with(‪$url, 'GET', $optionsSecondTryWithGET)
63  ->willThrowException($clientExceptionMock);
64  $subject = new ‪ExternalLinktype($requestFactoryMock);
65 
66  $result = $subject->checkLink(‪$url, [], $this->getMockBuilder(LinkAnalyzer::class)->disableOriginalConstructor()->getMock());
67 
68  self::assertFalse($result);
69  }
70 
71  #[Test]
73  {
74  $response = new Response(404);
75  $clientExceptionMock = $this->getMockBuilder(ClientException::class)->disableOriginalConstructor()->getMock();
76  $clientExceptionMock->expects(self::once())->method('hasResponse')->willReturn(true);
77  $clientExceptionMock->expects(self::once())->method('getResponse')->willReturn($response);
78 
79  $options = $this->‪getRequestHeaderOptions();
80 
81  ‪$url = 'https://example.org/~not-existing-url';
82  $requestFactoryMock = $this->getMockBuilder(RequestFactory::class)->disableOriginalConstructor()->getMock();
83  $requestFactoryMock->method('request')->with(‪$url, 'HEAD', $options)
84  ->willThrowException($clientExceptionMock);
85  $optionsSecondTryWithGET = array_merge_recursive($options, ['headers' => ['Range' => 'bytes=0-4048']]);
86  $requestFactoryMock->method('request')->with(‪$url, 'GET', $optionsSecondTryWithGET)
87  ->willThrowException($clientExceptionMock);
88 
89  $subject = new ‪ExternalLinktype($requestFactoryMock);
90 
91  $subject->checkLink(‪$url, [], $this->getMockBuilder(LinkAnalyzer::class)->disableOriginalConstructor()->getMock());
92  $errorParams = $subject->getErrorParams();
93 
94  self::assertSame($errorParams['errorType'], 'httpStatusCode');
95  self::assertSame($errorParams['errno'], 404);
96  }
97 
98  private function ‪getRequestHeaderOptions(): array
99  {
100  return [
101  'cookies' => new CookieJar(),
102  'allow_redirects' => ['strict' => true],
103  'headers' => [
104  'User-Agent' => 'TYPO3 linkvalidator',
105  'Accept' => '*/*',
106  'Accept-Language' => '*',
107  'Accept-Encoding' => '*',
108  ],
109  ];
110  }
111 
112  public static function ‪preprocessUrlsDataProvider(): \Generator
113  {
114  // regression test for issue #92230: handle incomplete or faulty URLs gracefully
115  yield 'faulty URL with mailto' => [
116  'mailto:http://example.org',
117  'mailto:http://example.org',
118  ];
119  yield 'Relative URL' => [
120  '/abc',
121  '/abc',
122  ];
123 
124  // regression tests for issues #89488, #89682
125  yield 'URL with query parameter and ampersand' => [
126  'https://standards.cen.eu/dyn/www/f?p=204:6:0::::FSP_ORG_ID,FSP_LANG_ID:,22&cs=1A3FFBC44FAB6B2A181C9525249C3A829',
127  'https://standards.cen.eu/dyn/www/f?p=204:6:0::::FSP_ORG_ID,FSP_LANG_ID:,22&cs=1A3FFBC44FAB6B2A181C9525249C3A829',
128  ];
129  yield 'URL with query parameter and ampersand with HTML entities' => [
130  'https://standards.cen.eu/dyn/www/f?p=204:6:0::::FSP_ORG_ID,FSP_LANG_ID:,22&amp;cs=1A3FFBC44FAB6B2A181C9525249C3A829',
131  'https://standards.cen.eu/dyn/www/f?p=204:6:0::::FSP_ORG_ID,FSP_LANG_ID:,22&cs=1A3FFBC44FAB6B2A181C9525249C3A829',
132  ];
133 
134  // regression tests for #89378
135  yield 'URL with path with dashes' => [
136  'https://example.com/Unternehmen/Ausbildung-Qualifikation/Weiterbildung-in-Niedersachsen/',
137  'https://example.com/Unternehmen/Ausbildung-Qualifikation/Weiterbildung-in-Niedersachsen/',
138  ];
139  yield 'URL with path with dashes (2)' => [
140  'https://example.com/startseite/wirtschaft/wirtschaftsfoerderung/beratung-foerderung/gruenderberatung/gruenderforen.html',
141  'https://example.com/startseite/wirtschaft/wirtschaftsfoerderung/beratung-foerderung/gruenderberatung/gruenderforen.html',
142  ];
143  yield 'URL with path with dashes (3)' => [
144  'http://example.com/universitaet/die-uni-im-ueberblick/lageplan/gebaeude/building/120',
145  'http://example.com/universitaet/die-uni-im-ueberblick/lageplan/gebaeude/building/120',
146  ];
147  yield 'URL with path and query parameters (including &, ~,; etc.)' => [
148  'http://example.com/tv?bcpid=1701167454001&amp;amp;amp;bckey=AQ~~,AAAAAGL7LqU~,aXlKNnCf9d9Tmck-kOc4PGFfCgHjM5JR&amp;amp;amp;bctid=1040702768001',
149  'http://example.com/tv?bcpid=1701167454001&amp;amp;bckey=AQ~~,AAAAAGL7LqU~,aXlKNnCf9d9Tmck-kOc4PGFfCgHjM5JR&amp;amp;bctid=1040702768001',
150  ];
151 
152  // make sure we correctly handle URLs with query parameters and fragment etc.
153  yield 'URL with query parameters, fragment, user, pass, port etc.' => [
154  'http://usr:pss@example.com:81/mypath/myfile.html?a=b&b[]=2&b[]=3#myfragment',
155  'http://usr:pss@example.com:81/mypath/myfile.html?a=b&b[]=2&b[]=3#myfragment',
156  ];
157  yield 'domain with special characters, URL with query parameters, fragment, user, pass, port etc.' => [
158  'http://usr:pss@äxample.com:81/mypath/myfile.html?a=b&b[]=2&b[]=3#myfragment',
159  'http://usr:pss@xn--xample-9ta.com:81/mypath/myfile.html?a=b&b[]=2&b[]=3#myfragment',
160  ];
161 
162  // domains with special characters: should be converted to punycode
163  yield 'domain with special characters' => [
164  'https://www.grün-example.org',
165  'https://www.xn--grn-example-uhb.org',
166  ];
167  yield 'domain with special characters and path' => [
168  'https://www.grün-example.org/a/bcd-efg/sfsfsfsfsf',
169  'https://www.xn--grn-example-uhb.org/a/bcd-efg/sfsfsfsfsf',
170  ];
171  }
172 
173  #[DataProvider('preprocessUrlsDataProvider')]
174  #[Test]
175  public function ‪preprocessUrlReturnsCorrectString(string $inputUrl, $expectedResult): void
176  {
178  $method = new \ReflectionMethod($subject, 'preprocessUrl');
179  $result = $method->invokeArgs($subject, [$inputUrl]);
180  self::assertEquals($result, $expectedResult);
181  }
182 
183  #[Test]
184  public function ‪setAdditionalConfigMergesHeaders(): void
185  {
186  $requestFactoryMock = $this->getMockBuilder(RequestFactory::class)->disableOriginalConstructor()->getMock();
187  $requestFactoryMock->expects(self::once())->method('request')->with(
188  'http://example.com',
189  'HEAD',
190  self::callback(static function ($result) {
191  return $result['headers']['X-MAS'] === 'Merry!' && $result['headers']['User-Agent'] === 'TYPO3 linkvalidator';
192  })
193  );
194 
195  $externalLinkType = new ‪ExternalLinktype($requestFactoryMock);
196  $externalLinkType->setAdditionalConfig(['headers.' => [
197  'X-MAS' => 'Merry!',
198  ]]);
199 
200  $externalLinkType->checkLink(
201  'http://example.com',
202  [],
203  $this->getMockBuilder(LinkAnalyzer::class)->disableOriginalConstructor()->getMock()
204  );
205  }
206 
211  #[Test]
213  {
214  $requestFactoryMock = $this->getMockBuilder(RequestFactory::class)->disableOriginalConstructor()->getMock();
215  $requestFactoryMock->expects(self::once())->method('request')->with(
216  'http://example.com',
217  'HEAD',
218  self::callback(static function ($result) {
219  if (isset($result['timeout'])) {
220  return false;
221  }
222  return true;
223  })
224  );
225 
226  $externalLinkType = new ‪ExternalLinktype($requestFactoryMock);
227  $externalLinkType->setAdditionalConfig([]);
228  $externalLinkType->checkLink(
229  'http://example.com',
230  [],
231  $this->getMockBuilder(LinkAnalyzer::class)->disableOriginalConstructor()->getMock()
232  );
233  }
234 
235  #[Test]
237  {
238  $requestFactoryMock = $this->getMockBuilder(RequestFactory::class)->disableOriginalConstructor()->getMock();
239  $requestFactoryMock->expects(self::once())->method('request')->with(
240  'http://example.com',
241  'HEAD',
242  self::callback(static function ($result) {
243  return $result['headers']['User-Agent'] === 'TYPO3 Testing';
244  })
245  );
246 
247  $externalLinktype = new ‪ExternalLinktype($requestFactoryMock);
248  $externalLinktype->setAdditionalConfig([
249  'httpAgentName' => 'TYPO3 Testing',
250  ]);
251 
252  $externalLinktype->checkLink(
253  'http://example.com',
254  [],
255  $this->getMockBuilder(LinkAnalyzer::class)->disableOriginalConstructor()->getMock()
256  );
257  }
258 
259  #[Test]
261  {
262  $requestFactoryMock = $this->getMockBuilder(RequestFactory::class)->disableOriginalConstructor()->getMock();
263  $requestFactoryMock->expects(self::once())->method('request')->with(
264  'http://example.com',
265  'HEAD',
266  self::callback(static function ($result) {
267  return $result['headers']['User-Agent'] === 'TYPO3 linkvalidator http://example.com';
268  })
269  );
270 
271  $externalLinkType = new ‪ExternalLinktype($requestFactoryMock);
272  $externalLinkType->setAdditionalConfig([
273  'httpAgentUrl' => 'http://example.com',
274  ]);
275 
276  $externalLinkType->checkLink(
277  'http://example.com',
278  [],
279  $this->getMockBuilder(LinkAnalyzer::class)->disableOriginalConstructor()->getMock()
280  );
281  }
282 
283  #[Test]
285  {
286  $requestFactoryMock = $this->getMockBuilder(RequestFactory::class)->disableOriginalConstructor()->getMock();
287  $requestFactoryMock->expects(self::once())->method('request')->with(
288  'http://example.com',
289  'HEAD',
290  self::callback(static function ($result) {
291  return $result['headers']['User-Agent'] === 'TYPO3 linkvalidator;mail@example.com';
292  })
293  );
294 
295  $externalLinktype = new ‪ExternalLinktype($requestFactoryMock);
296  $externalLinktype->setAdditionalConfig([
297  'httpAgentEmail' => 'mail@example.com',
298  ]);
299 
300  $externalLinktype->checkLink(
301  'http://example.com',
302  [],
303  $this->getMockBuilder(LinkAnalyzer::class)->disableOriginalConstructor()->getMock()
304  );
305  }
306 
307  #[Test]
309  {
310  $requestFactoryMock = $this->getMockBuilder(RequestFactory::class)->disableOriginalConstructor()->getMock();
311  $requestFactoryMock->expects(self::once())->method('request')->with(
312  'http://example.com',
313  'HEAD',
314  self::callback(static function ($result) {
315  return $result['headers']['User-Agent'] === 'TYPO3 linkvalidator;test@example.com';
316  })
317  );
318 
319  ‪$GLOBALS['TYPO3_CONF_VARS']['MAIL']['defaultMailFromAddress'] = 'test@example.com';
320 
321  $externalLinkType = new ‪ExternalLinktype($requestFactoryMock);
322  $externalLinkType->setAdditionalConfig([]);
323 
324  $externalLinkType->checkLink(
325  'http://example.com',
326  [],
327  $this->getMockBuilder(LinkAnalyzer::class)->disableOriginalConstructor()->getMock()
328  );
329  }
330 
331  #[Test]
333  {
334  $requestFactoryMock = $this->getMockBuilder(RequestFactory::class)->disableOriginalConstructor()->getMock();
335  $requestFactoryMock->expects(self::once())->method('request')->with(
336  'http://example.com',
337  'GET',
338  self::callback(static function ($result) {
339  return $result['headers']['Range'] === 'bytes=0-2048';
340  })
341  );
342 
343  ‪$GLOBALS['TYPO3_CONF_VARS']['MAIL']['defaultMailFromAddress'] = 'test@example.com';
344 
345  $externalLinkType = new ‪ExternalLinktype($requestFactoryMock);
346  $externalLinkType->setAdditionalConfig([
347  'method' => 'GET',
348  'range' => '0-2048',
349  ]);
350 
351  $externalLinkType->checkLink(
352  'http://example.com',
353  [],
354  $this->getMockBuilder(LinkAnalyzer::class)->disableOriginalConstructor()->getMock()
355  );
356  }
357 }
‪TYPO3\CMS\Linkvalidator\Tests\Unit\Linktype\ExternalLinktypeTest\setAdditionalConfigMergesHeaders
‪setAdditionalConfigMergesHeaders()
Definition: ExternalLinktypeTest.php:184
‪TYPO3\CMS\Linkvalidator\Tests\Unit\Linktype\ExternalLinktypeTest\setAdditionalConfigOverwritesUserAgent
‪setAdditionalConfigOverwritesUserAgent()
Definition: ExternalLinktypeTest.php:236
‪TYPO3\CMS\Linkvalidator\Tests\Unit\Linktype\ExternalLinktypeTest\preprocessUrlsDataProvider
‪static preprocessUrlsDataProvider()
Definition: ExternalLinktypeTest.php:112
‪TYPO3\CMS\Linkvalidator\Tests\Unit\Linktype\ExternalLinktypeTest\checkLinkWithExternalUrlNotFoundResultsNotFoundErrorType
‪checkLinkWithExternalUrlNotFoundResultsNotFoundErrorType()
Definition: ExternalLinktypeTest.php:72
‪TYPO3\CMS\Linkvalidator\Tests\Unit\Linktype\ExternalLinktypeTest\requestWithNoTimeoutIsCalledIfTimeoutNotSetByTsConfig
‪requestWithNoTimeoutIsCalledIfTimeoutNotSetByTsConfig()
Definition: ExternalLinktypeTest.php:212
‪TYPO3\CMS\Linkvalidator\Tests\Unit\Linktype\ExternalLinktypeTest\preprocessUrlReturnsCorrectString
‪preprocessUrlReturnsCorrectString(string $inputUrl, $expectedResult)
Definition: ExternalLinktypeTest.php:175
‪TYPO3\CMS\Core\Http\Client\GuzzleClientFactory
Definition: GuzzleClientFactory.php:28
‪TYPO3\CMS\Linkvalidator\Tests\Unit\Linktype\ExternalLinktypeTest\buildLanguageServiceMock
‪buildLanguageServiceMock()
Definition: ExternalLinktypeTest.php:41
‪TYPO3\CMS\Linkvalidator\Tests\Unit\Linktype\ExternalLinktypeTest\setUp
‪setUp()
Definition: ExternalLinktypeTest.php:35
‪TYPO3\CMS\Linkvalidator\Tests\Unit\Linktype\ExternalLinktypeTest
Definition: ExternalLinktypeTest.php:34
‪TYPO3\CMS\Linkvalidator\Tests\Unit\Linktype
Definition: ExternalLinktypeTest.php:18
‪TYPO3\CMS\Linkvalidator\Linktype\ExternalLinktype
Definition: ExternalLinktype.php:40
‪TYPO3\CMS\Core\Http\RequestFactory
Definition: RequestFactory.php:30
‪TYPO3\CMS\Linkvalidator\Tests\Unit\Linktype\ExternalLinktypeTest\setAdditionalConfigAppendsEmailFromGlobalsIfConfigured
‪setAdditionalConfigAppendsEmailFromGlobalsIfConfigured()
Definition: ExternalLinktypeTest.php:308
‪TYPO3\CMS\Webhooks\Message\$url
‪identifier readonly UriInterface $url
Definition: LoginErrorOccurredMessage.php:36
‪TYPO3\CMS\Linkvalidator\Tests\Unit\Linktype\ExternalLinktypeTest\setAdditionalConfigAppendsEmailIfConfigured
‪setAdditionalConfigAppendsEmailIfConfigured()
Definition: ExternalLinktypeTest.php:284
‪TYPO3\CMS\Linkvalidator\Tests\Unit\Linktype\ExternalLinktypeTest\setAdditionalConfigAppendsAgentUrlIfConfigured
‪setAdditionalConfigAppendsAgentUrlIfConfigured()
Definition: ExternalLinktypeTest.php:260
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Core\Localization\LanguageService
Definition: LanguageService.php:46
‪TYPO3\CMS\Linkvalidator\Tests\Unit\Linktype\ExternalLinktypeTest\getRequestHeaderOptions
‪getRequestHeaderOptions()
Definition: ExternalLinktypeTest.php:98
‪TYPO3\CMS\Linkvalidator\Tests\Unit\Linktype\ExternalLinktypeTest\setAdditionalConfigSetsRangeAndMethod
‪setAdditionalConfigSetsRangeAndMethod()
Definition: ExternalLinktypeTest.php:332
‪TYPO3\CMS\Linkvalidator\Tests\Unit\Linktype\ExternalLinktypeTest\checkLinkWithExternalUrlNotFoundReturnsFalse
‪checkLinkWithExternalUrlNotFoundReturnsFalse()
Definition: ExternalLinktypeTest.php:48