085b575ed04d1a0e96df13fe6fdee3d2c9836529
[Packages/TYPO3.CMS.git] / typo3 / sysext / frontend / Tests / Functional / SiteHandling / AbstractTestCase.php
1 <?php
2 declare(strict_types = 1);
3 namespace TYPO3\CMS\Frontend\Tests\Functional\SiteHandling;
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
18 use TYPO3\CMS\Core\Configuration\SiteConfiguration;
19 use TYPO3\CMS\Core\Utility\ArrayUtility;
20 use TYPO3\CMS\Frontend\Page\CacheHashCalculator;
21 use TYPO3\CMS\Frontend\Tests\Functional\SiteHandling\Fixtures\LinkHandlingController;
22 use TYPO3\CMS\Frontend\Tests\Functional\SiteHandling\Fixtures\PhpError;
23 use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\Internal\ArrayValueInstruction;
24 use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
25
26 /**
27 * Abstract test case for frontend requests
28 */
29 abstract class AbstractTestCase extends FunctionalTestCase
30 {
31 protected const ENCRYPTION_KEY = '4408d27a916d51e624b69af3554f516dbab61037a9f7b9fd6f81b4d3bedeccb6';
32
33 protected const TYPO3_CONF_VARS = [
34 'SYS' => [
35 'encryptionKey' => self::ENCRYPTION_KEY,
36 ],
37 'FE' => [
38 'cacheHash' => [
39 'requireCacheHashPresenceParameters' => ['testing[value]']
40 ],
41 ]
42 ];
43
44 protected const LANGUAGE_PRESETS = [
45 'EN' => ['id' => 0, 'title' => 'English', 'locale' => 'en_US.UTF8', 'iso' => 'en', 'hrefLang' => 'en-US', 'direction' => ''],
46 'FR' => ['id' => 1, 'title' => 'French', 'locale' => 'fr_FR.UTF8', 'iso' => 'fr', 'hrefLang' => 'fr-FR', 'direction' => ''],
47 'FR-CA' => ['id' => 2, 'title' => 'Franco-Canadian', 'locale' => 'fr_CA.UTF8', 'iso' => 'fr', 'hrefLang' => 'fr-CA', 'direction' => ''],
48 ];
49
50 /**
51 * @var string[]
52 */
53 protected $coreExtensionsToLoad = ['frontend', 'workspaces'];
54
55 /**
56 * @var string[]
57 */
58 protected $pathsToLinkInTestInstance = [
59 'typo3/sysext/core/Tests/Functional/Fixtures/Frontend/AdditionalConfiguration.php' => 'typo3conf/AdditionalConfiguration.php',
60 ];
61
62 /**
63 * Combines string values of multiple array as cross-product into flat items.
64 *
65 * Example:
66 * + meltStrings(['a','b'], ['c','e'], ['f','g'])
67 * + results into ['acf', 'acg', 'aef', 'aeg', 'bcf', 'bcg', 'bef', 'beg']
68 *
69 * @param array $arrays Distinct array that should be melted
70 * @param callable $finalCallback Callback being executed on last multiplier
71 * @param string $prefix Prefix containing concatenated previous values
72 * @return array
73 */
74 protected function meltStrings(array $arrays, callable $finalCallback = null, string $prefix = ''): array
75 {
76 $results = [];
77 $array = array_shift($arrays);
78 foreach ($array as $item) {
79 $resultItem = $prefix . $item;
80 if (count($arrays) > 0) {
81 $results = array_merge(
82 $results,
83 $this->meltStrings($arrays, $finalCallback, $resultItem)
84 );
85 continue;
86 }
87 if ($finalCallback !== null) {
88 $resultItem = call_user_func($finalCallback, $resultItem);
89 }
90 $results[] = $resultItem;
91 }
92 return $results;
93 }
94
95 /**
96 * @param array $array
97 * @return array
98 */
99 protected function wrapInArray(array $array): array
100 {
101 return array_map(
102 function ($item) {
103 return [$item];
104 },
105 $array
106 );
107 }
108
109 /**
110 * @param string[] $array
111 * @return array
112 */
113 protected function keysFromValues(array $array): array
114 {
115 return array_combine($array, $array);
116 }
117
118 /**
119 * Generates key names based on a template and array items as arguments.
120 *
121 * + keysFromTemplate([[1, 2, 3], [11, 22, 33]], '%1$d->%2$d (user:%3$d)')
122 * + returns the following array with generated keys
123 * [
124 * '1->2 (user:3)' => [1, 2, 3],
125 * '11->22 (user:33)' => [11, 22, 33],
126 * ]
127 *
128 * @param array $array
129 * @param string $template
130 * @param callable|null $callback
131 * @return array
132 */
133 protected function keysFromTemplate(array $array, string $template, callable $callback = null): array
134 {
135 $keys = array_unique(
136 array_map(
137 function (array $values) use ($template, $callback) {
138 if ($callback !== null) {
139 $values = call_user_func($callback, $values);
140 }
141 return vsprintf($template, $values);
142 },
143 $array
144 )
145 );
146
147 if (count($keys) !== count($array)) {
148 throw new \LogicException(
149 'Amount of generated keys does not match to item count.',
150 1534682840
151 );
152 }
153
154 return array_combine($keys, $array);
155 }
156
157 /**
158 * @param array $items
159 */
160 protected static function failIfArrayIsNotEmpty(array $items): void
161 {
162 if (empty($items)) {
163 return;
164 }
165
166 static::fail(
167 'Array was not empty as expected, but contained these items:' . LF
168 . '* ' . implode(LF . '* ', $items)
169 );
170 }
171
172 /**
173 * @param string $identifier
174 * @param array $site
175 * @param array $languages
176 * @param array $errorHandling
177 */
178 protected function writeSiteConfiguration(
179 string $identifier,
180 array $site = [],
181 array $languages = [],
182 array $errorHandling = []
183 ) {
184 $configuration = $site;
185 if (!empty($languages)) {
186 $configuration['languages'] = $languages;
187 }
188 if (!empty($errorHandling)) {
189 $configuration['errorHandling'] = $errorHandling;
190 }
191
192 $siteConfiguration = new SiteConfiguration(
193 $this->instancePath . '/typo3conf/sites/'
194 );
195
196 try {
197 $siteConfiguration->write($identifier, $configuration);
198 } catch (\Exception $exception) {
199 $this->markTestSkipped($exception->getMessage());
200 }
201 }
202
203 /**
204 * @param string $identifier
205 * @param array $overrides
206 */
207 protected function mergeSiteConfiguration(
208 string $identifier,
209 array $overrides
210 ) {
211 $siteConfiguration = new SiteConfiguration(
212 $this->instancePath . '/typo3conf/sites/'
213 );
214 $configuration = $siteConfiguration->load($identifier);
215 $configuration = array_merge($configuration, $overrides);
216 try {
217 $siteConfiguration->write($identifier, $configuration);
218 } catch (\Exception $exception) {
219 $this->markTestSkipped($exception->getMessage());
220 }
221 }
222
223 /**
224 * @param int $rootPageId
225 * @param string $base
226 * @return array
227 */
228 protected function buildSiteConfiguration(
229 int $rootPageId,
230 string $base = ''
231 ): array {
232 return [
233 'rootPageId' => $rootPageId,
234 'base' => $base,
235 ];
236 }
237
238 /**
239 * @param string $identifier
240 * @param string $base
241 * @return array
242 */
243 protected function buildDefaultLanguageConfiguration(
244 string $identifier,
245 string $base
246 ): array {
247 $configuration = $this->buildLanguageConfiguration($identifier, $base);
248 $configuration['typo3Language'] = 'default';
249 $configuration['flag'] = 'global';
250 unset($configuration['fallbackType'], $configuration['fallbacks']);
251 return $configuration;
252 }
253
254 /**
255 * @param string $identifier
256 * @param string $base
257 * @param array $fallbackIdentifiers
258 * @return array
259 */
260 protected function buildLanguageConfiguration(
261 string $identifier,
262 string $base,
263 array $fallbackIdentifiers = []
264 ): array {
265 $preset = $this->resolveLanguagePreset($identifier);
266
267 $configuration = [
268 'languageId' => $preset['id'],
269 'title' => $preset['title'],
270 'navigationTitle' => $preset['title'],
271 'base' => $base,
272 'locale' => $preset['locale'],
273 'iso-639-1' => $preset['iso'],
274 'hreflang' => $preset['hrefLang'],
275 'direction' => $preset['direction'],
276 'typo3Language' => $preset['iso'],
277 'flag' => $preset['iso'],
278 'fallbackType' => 'strict',
279 ];
280
281 if (!empty($fallbackIdentifiers)) {
282 $fallbackIds = array_map(
283 function (string $fallbackIdentifier) {
284 $preset = $this->resolveLanguagePreset($fallbackIdentifier);
285 return $preset['id'];
286 },
287 $fallbackIdentifiers
288 );
289 $configuration['fallbackType'] = 'fallback';
290 $configuration['fallbacks'] = implode(',', $fallbackIds);
291 }
292
293 return $configuration;
294 }
295
296 /**
297 * @param string $handler
298 * @param array $codes
299 * @return array
300 */
301 protected function buildErrorHandlingConfiguration(
302 string $handler,
303 array $codes
304 ): array {
305 if ($handler === 'Page') {
306 $baseConfiguration = [
307 'errorContentSource' => '404',
308 ];
309 } elseif ($handler === 'Fluid') {
310 $baseConfiguration = [
311 'errorFluidTemplate' => 'typo3/sysext/frontend/Tests/Functional/SiteHandling/Fixtures/FluidError.html',
312 'errorFluidTemplatesRootPath' => '',
313 'errorFluidLayoutsRootPath' => '',
314 'errorFluidPartialsRootPath' => '',
315 ];
316 } elseif ($handler === 'PHP') {
317 $baseConfiguration = [
318 'errorPhpClassFQCN' => PhpError::class,
319 ];
320 } else {
321 throw new \LogicException(
322 sprintf('Invalid handler "%s"', $handler),
323 1533894782
324 );
325 }
326
327 $baseConfiguration['errorHandler'] = $handler;
328
329 return array_map(
330 function (int $code) use ($baseConfiguration) {
331 $baseConfiguration['errorCode'] = $code;
332 return $baseConfiguration;
333 },
334 $codes
335 );
336 }
337
338 /**
339 * @param string $identifier
340 * @return mixed
341 */
342 protected function resolveLanguagePreset(string $identifier)
343 {
344 if (!isset(static::LANGUAGE_PRESETS[$identifier])) {
345 throw new \LogicException(
346 sprintf('Undefined preset identifier "%s"', $identifier),
347 1533893665
348 );
349 }
350 return static::LANGUAGE_PRESETS[$identifier];
351 }
352
353 /**
354 * @param string $uri
355 * @return string
356 */
357 protected static function generateCacheHash(string $uri): string
358 {
359 if (!isset($GLOBALS['TYPO3_CONF_VARS'])) {
360 $GLOBALS['TYPO3_CONF_VARS'] = [];
361 }
362
363 $configuration = $GLOBALS['TYPO3_CONF_VARS'];
364 ArrayUtility::mergeRecursiveWithOverrule(
365 $GLOBALS['TYPO3_CONF_VARS'],
366 static::TYPO3_CONF_VARS
367 );
368
369 $calculator = new CacheHashCalculator();
370 $parameters = $calculator->getRelevantParameters(
371 parse_url($uri, PHP_URL_QUERY)
372 );
373 $cacheHash = $calculator->calculateCacheHash($parameters);
374
375 $GLOBALS['TYPO3_CONF_VARS'] = $configuration;
376 return $cacheHash;
377 }
378
379 /**
380 * @param array $typoScript
381 * @return ArrayValueInstruction
382 */
383 protected function createTypoLinkUrlInstruction(array $typoScript): ArrayValueInstruction
384 {
385 return (new ArrayValueInstruction(LinkHandlingController::class))
386 ->withArray([
387 '10' => 'TEXT',
388 '10.' => [
389 'typolink.' => array_merge(
390 $typoScript,
391 ['returnLast' => 'url']
392 )
393 ]
394 ]);
395 }
396
397 /**
398 * @param array $typoScript
399 * @return ArrayValueInstruction
400 */
401 protected function createHierarchicalMenuProcessorInstruction(array $typoScript): ArrayValueInstruction
402 {
403 return (new ArrayValueInstruction(LinkHandlingController::class))
404 ->withArray([
405 '10' => 'FLUIDTEMPLATE',
406 '10.' => [
407 'file' => 'typo3/sysext/frontend/Tests/Functional/SiteHandling/Fixtures/FluidJson.html',
408 'dataProcessing.' => [
409 '1' => 'TYPO3\\CMS\\Frontend\\DataProcessing\\MenuProcessor',
410 '1.' => array_merge(
411 $typoScript,
412 ['as' => 'results']
413 )
414 ],
415 ],
416 ]);
417 }
418
419 /**
420 * @param array $typoScript
421 * @return ArrayValueInstruction
422 */
423 protected function createLanguageMenuProcessorInstruction(array $typoScript): ArrayValueInstruction
424 {
425 return (new ArrayValueInstruction(LinkHandlingController::class))
426 ->withArray([
427 '10' => 'FLUIDTEMPLATE',
428 '10.' => [
429 'file' => 'typo3/sysext/frontend/Tests/Functional/SiteHandling/Fixtures/FluidJson.html',
430 'dataProcessing.' => [
431 '1' => 'TYPO3\\CMS\\Frontend\\DataProcessing\\LanguageMenuProcessor',
432 '1.' => array_merge(
433 $typoScript,
434 ['as' => 'results']
435 )
436 ],
437 ],
438 ]);
439 }
440
441 /**
442 * Filters and keeps only desired names.
443 *
444 * @param array $menu
445 * @param array $keepNames
446 * @return array
447 */
448 protected function filterMenu(
449 array $menu,
450 array $keepNames = ['title', 'link']
451 ): array {
452 if (!in_array('children', $keepNames)) {
453 $keepNames[] = 'children';
454 }
455 return array_map(
456 function (array $menuItem) use ($keepNames) {
457 $menuItem = array_filter(
458 $menuItem,
459 function (string $name) use ($keepNames) {
460 return in_array($name, $keepNames);
461 },
462 ARRAY_FILTER_USE_KEY
463 );
464 if (is_array($menuItem['children'] ?? null)) {
465 $menuItem['children'] = $this->filterMenu(
466 $menuItem['children'],
467 $keepNames
468 );
469 }
470 return $menuItem;
471 },
472 $menu
473 );
474 }
475
476 /**
477 * @param array $keys
478 * @param mixed $payload
479 * @return array
480 */
481 protected function populateToKeys(array $keys, $payload): array
482 {
483 $result = [];
484 foreach ($keys as $key) {
485 $result[$key] = $payload;
486 }
487 return $result;
488 }
489 }