1<?php
2
3declare(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
18namespace TYPO3\CMS\Install\Controller;
19
20use Psr\Http\Message\ResponseInterface;
21use Psr\Http\Message\ServerRequestInterface;
22use Symfony\Component\Mime\Address;
23use Symfony\Component\Mime\Exception\RfcComplianceException;
24use TYPO3\CMS\Backend\Toolbar\Enumeration\InformationStatus;
25use TYPO3\CMS\Core\Core\Environment;
26use TYPO3\CMS\Core\Database\ConnectionPool;
27use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
28use TYPO3\CMS\Core\FormProtection\InstallToolFormProtection;
29use TYPO3\CMS\Core\Http\JsonResponse;
30use TYPO3\CMS\Core\Imaging\GraphicalFunctions;
31use TYPO3\CMS\Core\Mail\FluidEmail;
32use TYPO3\CMS\Core\Mail\Mailer;
33use TYPO3\CMS\Core\Messaging\FlashMessage;
34use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
35use TYPO3\CMS\Core\Utility\CommandUtility;
36use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
37use TYPO3\CMS\Core\Utility\GeneralUtility;
38use TYPO3\CMS\Core\Utility\MathUtility;
39use TYPO3\CMS\Core\Utility\StringUtility;
40use TYPO3\CMS\Install\FolderStructure\DefaultFactory;
41use TYPO3\CMS\Install\FolderStructure\DefaultPermissionsCheck;
42use TYPO3\CMS\Install\Service\LateBootService;
43use TYPO3\CMS\Install\SystemEnvironment\Check;
44use TYPO3\CMS\Install\SystemEnvironment\DatabaseCheck;
45use TYPO3\CMS\Install\SystemEnvironment\ServerResponse\ServerResponseCheck;
46use TYPO3\CMS\Install\SystemEnvironment\SetupCheck;
47
48/**
49 * Environment controller
50 * @internal This class is a specific controller implementation and is not considered part of the Public TYPO3 API.
51 */
52class EnvironmentController extends AbstractController
53{
54    private const IMAGE_FILE_EXT = ['gif', 'jpg', 'png', 'tif', 'ai', 'pdf', 'webp'];
55    private const TEST_REFERENCE_PATH = __DIR__ . '/../../Resources/Public/Images/TestReference';
56
57    /**
58     * @var LateBootService
59     */
60    private $lateBootService;
61
62    public function __construct(
63        LateBootService $lateBootService
64    ) {
65        $this->lateBootService = $lateBootService;
66    }
67
68    /**
69     * Main "show the cards" view
70     *
71     * @param ServerRequestInterface $request
72     * @return ResponseInterface
73     */
74    public function cardsAction(ServerRequestInterface $request): ResponseInterface
75    {
76        $view = $this->initializeStandaloneView($request, 'Environment/Cards.html');
77        return new JsonResponse([
78            'success' => true,
79            'html' => $view->render(),
80        ]);
81    }
82
83    /**
84     * System Information Get Data action
85     *
86     * @param ServerRequestInterface $request
87     * @return ResponseInterface
88     */
89    public function systemInformationGetDataAction(ServerRequestInterface $request): ResponseInterface
90    {
91        $view = $this->initializeStandaloneView($request, 'Environment/SystemInformation.html');
92        $view->assignMultiple([
93            'systemInformationCgiDetected' => Environment::isRunningOnCgiServer(),
94            'systemInformationDatabaseConnections' => $this->getDatabaseConnectionInformation(),
95            'systemInformationOperatingSystem' => Environment::isWindows() ? 'Windows' : 'Unix',
96            'systemInformationApplicationContext' => $this->getApplicationContextInformation(),
97            'phpVersion' => PHP_VERSION,
98        ]);
99        return new JsonResponse([
100            'success' => true,
101            'html' => $view->render(),
102        ]);
103    }
104
105    /**
106     * System Information Get Data action
107     *
108     * @param ServerRequestInterface $request
109     * @return ResponseInterface
110     */
111    public function phpInfoGetDataAction(ServerRequestInterface $request): ResponseInterface
112    {
113        $view = $this->initializeStandaloneView($request, 'Environment/PhpInfo.html');
114        return new JsonResponse([
115            'success' => true,
116            'html' => $view->render(),
117        ]);
118    }
119
120    /**
121     * Get environment status
122     *
123     * @param ServerRequestInterface $request
124     * @return ResponseInterface
125     */
126    public function environmentCheckGetStatusAction(ServerRequestInterface $request): ResponseInterface
127    {
128        $view = $this->initializeStandaloneView($request, 'Environment/EnvironmentCheck.html');
129        $messageQueue = new FlashMessageQueue('install');
130        $checkMessages = (new Check())->getStatus();
131        foreach ($checkMessages as $message) {
132            $messageQueue->enqueue($message);
133        }
134        $setupMessages = (new SetupCheck())->getStatus();
135        foreach ($setupMessages as $message) {
136            $messageQueue->enqueue($message);
137        }
138        $databaseMessages = (new DatabaseCheck())->getStatus();
139        foreach ($databaseMessages as $message) {
140            $messageQueue->enqueue($message);
141        }
142        $serverResponseMessages = (new ServerResponseCheck(false))->getStatus();
143        foreach ($serverResponseMessages as $message) {
144            $messageQueue->enqueue($message);
145        }
146        return new JsonResponse([
147            'success' => true,
148            'status' => [
149                'error' => $messageQueue->getAllMessages(FlashMessage::ERROR),
150                'warning' => $messageQueue->getAllMessages(FlashMessage::WARNING),
151                'ok' => $messageQueue->getAllMessages(FlashMessage::OK),
152                'information' => $messageQueue->getAllMessages(FlashMessage::INFO),
153                'notice' => $messageQueue->getAllMessages(FlashMessage::NOTICE),
154            ],
155            'html' => $view->render(),
156            'buttons' => [
157                [
158                    'btnClass' => 'btn-default t3js-environmentCheck-execute',
159                    'text' => 'Run tests again',
160                ],
161            ],
162        ]);
163    }
164
165    /**
166     * Get folder structure status
167     *
168     * @param ServerRequestInterface $request
169     * @return ResponseInterface
170     */
171    public function folderStructureGetStatusAction(ServerRequestInterface $request): ResponseInterface
172    {
173        $view = $this->initializeStandaloneView($request, 'Environment/FolderStructure.html');
174        $folderStructureFactory = GeneralUtility::makeInstance(DefaultFactory::class);
175        $structureFacade = $folderStructureFactory->getStructure();
176
177        $structureMessages = $structureFacade->getStatus();
178        $errorQueue = new FlashMessageQueue('install');
179        $okQueue = new FlashMessageQueue('install');
180        foreach ($structureMessages as $message) {
181            if ($message->getSeverity() === FlashMessage::ERROR
182                || $message->getSeverity() === FlashMessage::WARNING
183            ) {
184                $errorQueue->enqueue($message);
185            } else {
186                $okQueue->enqueue($message);
187            }
188        }
189
190        $permissionCheck = GeneralUtility::makeInstance(DefaultPermissionsCheck::class);
191
192        $view->assign('publicPath', Environment::getPublicPath());
193
194        $buttons = [];
195        if ($errorQueue->count() > 0) {
196            $buttons[] = [
197                'btnClass' => 'btn-default t3js-folderStructure-errors-fix',
198                'text' => 'Try to fix file and folder permissions',
199            ];
200        }
201
202        return new JsonResponse([
203            'success' => true,
204            'errorStatus' => $errorQueue,
205            'okStatus' => $okQueue,
206            'folderStructureFilePermissionStatus' => $permissionCheck->getMaskStatus('fileCreateMask'),
207            'folderStructureDirectoryPermissionStatus' => $permissionCheck->getMaskStatus('folderCreateMask'),
208            'html' => $view->render(),
209            'buttons' => $buttons,
210        ]);
211    }
212
213    /**
214     * Try to fix folder structure errors
215     *
216     * @return ResponseInterface
217     */
218    public function folderStructureFixAction(): ResponseInterface
219    {
220        $folderStructureFactory = GeneralUtility::makeInstance(DefaultFactory::class);
221        $structureFacade = $folderStructureFactory->getStructure();
222        $fixedStatusObjects = $structureFacade->fix();
223        return new JsonResponse([
224            'success' => true,
225            'fixedStatus' => $fixedStatusObjects,
226        ]);
227    }
228
229    /**
230     * System Information Get Data action
231     *
232     * @param ServerRequestInterface $request
233     * @return ResponseInterface
234     */
235    public function mailTestGetDataAction(ServerRequestInterface $request): ResponseInterface
236    {
237        $view = $this->initializeStandaloneView($request, 'Environment/MailTest.html');
238        $formProtection = FormProtectionFactory::get(InstallToolFormProtection::class);
239        $view->assignMultiple([
240            'mailTestToken' => $formProtection->generateToken('installTool', 'mailTest'),
241            'mailTestSenderAddress' => $this->getSenderEmailAddress(),
242        ]);
243        return new JsonResponse([
244            'success' => true,
245            'html' => $view->render(),
246            'buttons' => [
247                [
248                    'btnClass' => 'btn-default t3js-mailTest-execute',
249                    'text' => 'Send test mail',
250                ],
251            ],
252        ]);
253    }
254
255    /**
256     *  Send a test mail
257     *
258     * @param ServerRequestInterface $request
259     * @return ResponseInterface
260     */
261    public function mailTestAction(ServerRequestInterface $request): ResponseInterface
262    {
263        $container = $this->lateBootService->getContainer();
264        $backup = $this->lateBootService->makeCurrent($container);
265        $messages = new FlashMessageQueue('install');
266        $recipient = $request->getParsedBody()['install']['email'];
267        if (empty($recipient) || !GeneralUtility::validEmail($recipient)) {
268            $messages->enqueue(new FlashMessage(
269                'Given address is not a valid email address.',
270                'Mail not sent',
271                FlashMessage::ERROR
272            ));
273        } else {
274            try {
275                $variables = [
276                    'headline' => 'TYPO3 Test Mail',
277                    'introduction' => 'Hey TYPO3 Administrator',
278                    'content' => 'Seems like your favorite TYPO3 installation can send out emails!',
279                ];
280                $mailMessage = GeneralUtility::makeInstance(FluidEmail::class);
281                $mailMessage
282                    ->to($recipient)
283                    ->from(new Address($this->getSenderEmailAddress(), $this->getSenderEmailName()))
284                    ->subject($this->getEmailSubject())
285                    ->setRequest($request)
286                    ->assignMultiple($variables);
287
288                GeneralUtility::makeInstance(Mailer::class)->send($mailMessage);
289                $messages->enqueue(new FlashMessage(
290                    'Recipient: ' . $recipient,
291                    'Test mail sent'
292                ));
293            } catch (RfcComplianceException $exception) {
294                $messages->enqueue(new FlashMessage(
295                    'Please verify $GLOBALS[\'TYPO3_CONF_VARS\'][\'MAIL\'][\'defaultMailFromAddress\'] is a valid mail address.'
296                    . ' Error message: ' . $exception->getMessage(),
297                    'RFC compliance problem',
298                    FlashMessage::ERROR
299                ));
300            } catch (\Throwable $throwable) {
301                $messages->enqueue(new FlashMessage(
302                    'Please verify $GLOBALS[\'TYPO3_CONF_VARS\'][\'MAIL\'][*] settings are valid.'
303                    . ' Error message: ' . $throwable->getMessage(),
304                    'Could not deliver mail',
305                    FlashMessage::ERROR
306                ));
307            }
308        }
309        $this->lateBootService->makeCurrent(null, $backup);
310        return new JsonResponse([
311            'success' => true,
312            'status' => $messages,
313        ]);
314    }
315
316    /**
317     * System Information Get Data action
318     *
319     * @param ServerRequestInterface $request
320     * @return ResponseInterface
321     */
322    public function imageProcessingGetDataAction(ServerRequestInterface $request): ResponseInterface
323    {
324        $view = $this->initializeStandaloneView($request, 'Environment/ImageProcessing.html');
325        $view->assignMultiple([
326            'imageProcessingProcessor' => $GLOBALS['TYPO3_CONF_VARS']['GFX']['processor'] === 'GraphicsMagick' ? 'GraphicsMagick' : 'ImageMagick',
327            'imageProcessingEnabled' => $GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_enabled'],
328            'imageProcessingPath' => $GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_path'],
329            'imageProcessingVersion' => $this->determineImageMagickVersion(),
330            'imageProcessingEffects' => $GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_effects'],
331            'imageProcessingGdlibEnabled' => $GLOBALS['TYPO3_CONF_VARS']['GFX']['gdlib'],
332            'imageProcessingGdlibPng' => $GLOBALS['TYPO3_CONF_VARS']['GFX']['gdlib_png'],
333            'imageProcessingFileFormats' => $GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'],
334        ]);
335        return new JsonResponse([
336            'success' => true,
337            'html' => $view->render(),
338            'buttons' => [
339                [
340                    'btnClass' => 'btn-default disabled t3js-imageProcessing-execute',
341                    'text' => 'Run image tests again',
342                ],
343            ],
344        ]);
345    }
346
347    /**
348     * Create true type font test image
349     *
350     * @return ResponseInterface
351     */
352    public function imageProcessingTrueTypeAction(): ResponseInterface
353    {
354        $image = @imagecreate(200, 50);
355        imagecolorallocate($image, 255, 255, 55);
356        $textColor = imagecolorallocate($image, 233, 14, 91);
357        @imagettftext(
358            $image,
359            20 / 96.0 * 72, // As in compensateFontSizeBasedOnFreetypeDpi
360            0,
361            10,
362            20,
363            $textColor,
364            ExtensionManagementUtility::extPath('install') . 'Resources/Private/Font/vera.ttf',
365            'Testing true type'
366        );
367        $outputFile = Environment::getPublicPath() . '/typo3temp/assets/images/installTool-' . StringUtility::getUniqueId('createTrueTypeFontTestImage') . '.gif';
368        @imagegif($image, $outputFile);
369        $fileExists = file_exists($outputFile);
370        if ($fileExists) {
371            GeneralUtility::fixPermissions($outputFile);
372        }
373        $result = [
374            'fileExists' => $fileExists,
375            'referenceFile' => self::TEST_REFERENCE_PATH . '/Font.gif',
376        ];
377        if ($fileExists) {
378            $result['outputFile'] = $outputFile;
379        }
380        return $this->getImageTestResponse($result);
381    }
382
383    /**
384     * Convert to jpg from jpg
385     *
386     * @return ResponseInterface
387     */
388    public function imageProcessingReadJpgAction(): ResponseInterface
389    {
390        return $this->convertImageFormatsToJpg('jpg');
391    }
392
393    /**
394     * Convert to jpg from gif
395     *
396     * @return ResponseInterface
397     */
398    public function imageProcessingReadGifAction(): ResponseInterface
399    {
400        return $this->convertImageFormatsToJpg('gif');
401    }
402
403    /**
404     * Convert to jpg from png
405     *
406     * @return ResponseInterface
407     */
408    public function imageProcessingReadPngAction(): ResponseInterface
409    {
410        return $this->convertImageFormatsToJpg('png');
411    }
412
413    /**
414     * Convert to jpg from tif
415     *
416     * @return ResponseInterface
417     */
418    public function imageProcessingReadTifAction(): ResponseInterface
419    {
420        return $this->convertImageFormatsToJpg('tif');
421    }
422
423    /**
424     * Convert to jpg from pdf
425     *
426     * @return ResponseInterface
427     */
428    public function imageProcessingReadPdfAction(): ResponseInterface
429    {
430        return $this->convertImageFormatsToJpg('pdf');
431    }
432
433    /**
434     * Convert to jpg from ai
435     *
436     * @return ResponseInterface
437     */
438    public function imageProcessingReadAiAction(): ResponseInterface
439    {
440        return $this->convertImageFormatsToJpg('ai');
441    }
442
443    /**
444     * Writing gif test
445     *
446     * @return ResponseInterface
447     */
448    public function imageProcessingWriteGifAction(): ResponseInterface
449    {
450        if (!$this->isImageMagickEnabledAndConfigured()) {
451            return new JsonResponse([
452                'status' => [$this->imageMagickDisabledMessage()],
453            ]);
454        }
455        $imageBasePath = ExtensionManagementUtility::extPath('install') . 'Resources/Public/Images/';
456        $inputFile = $imageBasePath . 'TestInput/Test.gif';
457        $imageProcessor = $this->initializeImageProcessor();
458        $imageProcessor->imageMagickConvert_forceFileNameBody = StringUtility::getUniqueId('write-gif');
459        $imResult = $imageProcessor->imageMagickConvert($inputFile, 'gif', '300', '', '', '', [], true);
460        $messages = new FlashMessageQueue('install');
461        if ($imResult !== null && is_file($imResult[3])) {
462            if ($GLOBALS['TYPO3_CONF_VARS']['GFX']['gif_compress']) {
463                clearstatcache();
464                $previousSize = GeneralUtility::formatSize((int)filesize($imResult[3]));
465                $methodUsed = GraphicalFunctions::gifCompress($imResult[3], '');
466                clearstatcache();
467                $compressedSize = GeneralUtility::formatSize((int)filesize($imResult[3]));
468                $messages->enqueue(new FlashMessage(
469                    'Method used by compress: ' . $methodUsed . LF
470                    . ' Previous filesize: ' . $previousSize . '. Current filesize:' . $compressedSize,
471                    'Compressed gif',
472                    FlashMessage::INFO
473                ));
474            } else {
475                $messages->enqueue(new FlashMessage(
476                    '',
477                    'Gif compression not enabled by [GFX][gif_compress]',
478                    FlashMessage::INFO
479                ));
480            }
481            $result = [
482                'status' => $messages,
483                'fileExists' => true,
484                'outputFile' => $imResult[3],
485                'referenceFile' => self::TEST_REFERENCE_PATH . '/Write-gif.gif',
486                'command' => $imageProcessor->IM_commands,
487            ];
488        } else {
489            $result = [
490                'status' => [$this->imageGenerationFailedMessage()],
491                'command' => $imageProcessor->IM_commands,
492            ];
493        }
494        return $this->getImageTestResponse($result);
495    }
496
497    /**
498     * Writing png test
499     *
500     * @return ResponseInterface
501     */
502    public function imageProcessingWritePngAction(): ResponseInterface
503    {
504        if (!$this->isImageMagickEnabledAndConfigured()) {
505            return new JsonResponse([
506                'status' => [$this->imageMagickDisabledMessage()],
507            ]);
508        }
509        $imageBasePath = ExtensionManagementUtility::extPath('install') . 'Resources/Public/Images/';
510        $inputFile = $imageBasePath . 'TestInput/Test.png';
511        $imageProcessor = $this->initializeImageProcessor();
512        $imageProcessor->imageMagickConvert_forceFileNameBody = StringUtility::getUniqueId('write-png');
513        $imResult = $imageProcessor->imageMagickConvert($inputFile, 'png', '300', '', '', '', [], true);
514        if ($imResult !== null && is_file($imResult[3])) {
515            $result = [
516                'fileExists' => true,
517                'outputFile' => $imResult[3],
518                'referenceFile' => self::TEST_REFERENCE_PATH . '/Write-png.png',
519                'command' => $imageProcessor->IM_commands,
520            ];
521        } else {
522            $result = [
523                'status' => [$this->imageGenerationFailedMessage()],
524                'command' => $imageProcessor->IM_commands,
525            ];
526        }
527        return $this->getImageTestResponse($result);
528    }
529    /**
530     * Writing webp test
531     *
532     * @return ResponseInterface
533     */
534    public function imageProcessingWriteWebpAction(): ResponseInterface
535    {
536        if (!$this->isImageMagickEnabledAndConfigured()) {
537            return new JsonResponse([
538                'status' => [$this->imageMagickDisabledMessage()],
539            ]);
540        }
541        $imageBasePath = ExtensionManagementUtility::extPath('install') . 'Resources/Public/Images/';
542        $inputFile = $imageBasePath . 'TestInput/Test.webp';
543        $imageProcessor = $this->initializeImageProcessor();
544        $imageProcessor->imageMagickConvert_forceFileNameBody = StringUtility::getUniqueId('write-webp');
545        $imResult = $imageProcessor->imageMagickConvert($inputFile, 'webp', '300', '', '', '', [], true);
546        if ($imResult !== null && is_file($imResult[3])) {
547            $result = [
548                'fileExists' => true,
549                'outputFile' => $imResult[3],
550                'referenceFile' => self::TEST_REFERENCE_PATH . '/Write-webp.webp',
551                'command' => $imageProcessor->IM_commands,
552            ];
553        } else {
554            $result = [
555                'status' => [$this->imageGenerationFailedMessage()],
556                'command' => $imageProcessor->IM_commands,
557            ];
558        }
559        return $this->getImageTestResponse($result);
560    }
561
562    /**
563     * Scaling transparent files - gif to gif
564     *
565     * @return ResponseInterface
566     */
567    public function imageProcessingGifToGifAction(): ResponseInterface
568    {
569        if (!$this->isImageMagickEnabledAndConfigured()) {
570            return new JsonResponse([
571                'status' => [$this->imageMagickDisabledMessage()],
572            ]);
573        }
574        $imageBasePath = ExtensionManagementUtility::extPath('install') . 'Resources/Public/Images/';
575        $imageProcessor = $this->initializeImageProcessor();
576        $inputFile = $imageBasePath . 'TestInput/Transparent.gif';
577        $imageProcessor->imageMagickConvert_forceFileNameBody = StringUtility::getUniqueId('scale-gif');
578        $imResult = $imageProcessor->imageMagickConvert($inputFile, 'gif', '300', '', '', '', [], true);
579        if ($imResult !== null && file_exists($imResult[3])) {
580            $result = [
581                'fileExists' => true,
582                'outputFile' => $imResult[3],
583                'referenceFile' => self::TEST_REFERENCE_PATH . '/Scale-gif.gif',
584                'command' => $imageProcessor->IM_commands,
585            ];
586        } else {
587            $result = [
588                'status' => [$this->imageGenerationFailedMessage()],
589                'command' => $imageProcessor->IM_commands,
590            ];
591        }
592        return $this->getImageTestResponse($result);
593    }
594
595    /**
596     * Scaling transparent files - png to png
597     *
598     * @return ResponseInterface
599     */
600    public function imageProcessingPngToPngAction(): ResponseInterface
601    {
602        if (!$this->isImageMagickEnabledAndConfigured()) {
603            return new JsonResponse([
604                'status' => [$this->imageMagickDisabledMessage()],
605            ]);
606        }
607        $imageBasePath = ExtensionManagementUtility::extPath('install') . 'Resources/Public/Images/';
608        $imageProcessor = $this->initializeImageProcessor();
609        $inputFile = $imageBasePath . 'TestInput/Transparent.png';
610        $imageProcessor->imageMagickConvert_forceFileNameBody = StringUtility::getUniqueId('scale-png');
611        $imResult = $imageProcessor->imageMagickConvert($inputFile, 'png', '300', '', '', '', [], true);
612        if ($imResult !== null && file_exists($imResult[3])) {
613            $result = [
614                'fileExists' => true,
615                'outputFile' => $imResult[3],
616                'referenceFile' => self::TEST_REFERENCE_PATH . '/Scale-png.png',
617                'command' => $imageProcessor->IM_commands,
618            ];
619        } else {
620            $result = [
621                'status' => [$this->imageGenerationFailedMessage()],
622                'command' => $imageProcessor->IM_commands,
623            ];
624        }
625        return $this->getImageTestResponse($result);
626    }
627
628    /**
629     * Scaling transparent files - gif to jpg
630     *
631     * @return ResponseInterface
632     */
633    public function imageProcessingGifToJpgAction(): ResponseInterface
634    {
635        if (!$this->isImageMagickEnabledAndConfigured()) {
636            return new JsonResponse([
637                'status' => [$this->imageMagickDisabledMessage()],
638            ]);
639        }
640        $imageBasePath = ExtensionManagementUtility::extPath('install') . 'Resources/Public/Images/';
641        $imageProcessor = $this->initializeImageProcessor();
642        $inputFile = $imageBasePath . 'TestInput/Transparent.gif';
643        $imageProcessor->imageMagickConvert_forceFileNameBody = StringUtility::getUniqueId('scale-jpg');
644        $jpegQuality = MathUtility::forceIntegerInRange($GLOBALS['TYPO3_CONF_VARS']['GFX']['jpg_quality'], 10, 100, 85);
645        $imResult = $imageProcessor->imageMagickConvert($inputFile, 'jpg', '300', '', '-quality ' . $jpegQuality . ' -opaque white -background white -flatten', '', [], true);
646        if ($imResult !== null && file_exists($imResult[3])) {
647            $result = [
648                'fileExists' => true,
649                'outputFile' => $imResult[3],
650                'referenceFile' => self::TEST_REFERENCE_PATH . '/Scale-jpg.jpg',
651                'command' => $imageProcessor->IM_commands,
652            ];
653        } else {
654            $result = [
655                'status' => [$this->imageGenerationFailedMessage()],
656                'command' => $imageProcessor->IM_commands,
657            ];
658        }
659        return $this->getImageTestResponse($result);
660    }
661
662    /**
663     * Converting jpg to webp
664     *
665     * @return ResponseInterface
666     */
667    public function imageProcessingJpgToWebpAction(): ResponseInterface
668    {
669        if (!$this->isImageMagickEnabledAndConfigured()) {
670            return new JsonResponse([
671                'status' => [$this->imageMagickDisabledMessage()],
672            ]);
673        }
674        $imageBasePath = ExtensionManagementUtility::extPath('install') . 'Resources/Public/Images/';
675        $imageProcessor = $this->initializeImageProcessor();
676        $inputFile = $imageBasePath . 'TestInput/Test.jpg';
677        $imageProcessor->imageMagickConvert_forceFileNameBody = StringUtility::getUniqueId('read-webp');
678        $imResult = $imageProcessor->imageMagickConvert($inputFile, 'webp', '300', '', '', '', [], true);
679        if ($imResult !== null) {
680            $result = [
681                'fileExists' => file_exists($imResult[3]),
682                'outputFile' => $imResult[3],
683                'referenceFile' => self::TEST_REFERENCE_PATH . '/Convert-webp.webp',
684                'command' => $imageProcessor->IM_commands,
685            ];
686        } else {
687            $result = [
688                'status' => [$this->imageGenerationFailedMessage()],
689                'command' => $imageProcessor->IM_commands,
690            ];
691        }
692        return $this->getImageTestResponse($result);
693    }
694
695    /**
696     * Combine images with gif mask
697     *
698     * @return ResponseInterface
699     */
700    public function imageProcessingCombineGifMaskAction(): ResponseInterface
701    {
702        if (!$this->isImageMagickEnabledAndConfigured()) {
703            return new JsonResponse([
704                'status' => [$this->imageMagickDisabledMessage()],
705            ]);
706        }
707        $imageBasePath = ExtensionManagementUtility::extPath('install') . 'Resources/Public/Images/';
708        $imageProcessor = $this->initializeImageProcessor();
709        $inputFile = $imageBasePath . 'TestInput/BackgroundOrange.gif';
710        $overlayFile = $imageBasePath . 'TestInput/Test.jpg';
711        $maskFile = $imageBasePath . 'TestInput/MaskBlackWhite.gif';
712        $resultFile = $this->getImagesPath() . $imageProcessor->filenamePrefix
713            . StringUtility::getUniqueId($imageProcessor->alternativeOutputKey . 'combine1') . '.jpg';
714        $imageProcessor->combineExec($inputFile, $overlayFile, $maskFile, $resultFile);
715        $imResult = $imageProcessor->getImageDimensions($resultFile);
716        if ($imResult) {
717            $result = [
718                'fileExists' => true,
719                'outputFile' => $imResult[3],
720                'referenceFile' => self::TEST_REFERENCE_PATH . '/Combine-1.jpg',
721                'command' => $imageProcessor->IM_commands,
722            ];
723        } else {
724            $result = [
725                'status' => [$this->imageGenerationFailedMessage()],
726                'command' => $imageProcessor->IM_commands,
727            ];
728        }
729        return $this->getImageTestResponse($result);
730    }
731
732    /**
733     * Combine images with jpg mask
734     *
735     * @return ResponseInterface
736     */
737    public function imageProcessingCombineJpgMaskAction(): ResponseInterface
738    {
739        if (!$this->isImageMagickEnabledAndConfigured()) {
740            return new JsonResponse([
741                'status' => [$this->imageMagickDisabledMessage()],
742            ]);
743        }
744        $imageBasePath = ExtensionManagementUtility::extPath('install') . 'Resources/Public/Images/';
745        $imageProcessor = $this->initializeImageProcessor();
746        $inputFile = $imageBasePath . 'TestInput/BackgroundCombine.jpg';
747        $overlayFile = $imageBasePath . 'TestInput/Test.jpg';
748        $maskFile = $imageBasePath . 'TestInput/MaskCombine.jpg';
749        $resultFile = $this->getImagesPath() . $imageProcessor->filenamePrefix
750            . StringUtility::getUniqueId($imageProcessor->alternativeOutputKey . 'combine2') . '.jpg';
751        $imageProcessor->combineExec($inputFile, $overlayFile, $maskFile, $resultFile);
752        $imResult = $imageProcessor->getImageDimensions($resultFile);
753        if ($imResult) {
754            $result = [
755                'fileExists' => true,
756                'outputFile' => $imResult[3],
757                'referenceFile' => self::TEST_REFERENCE_PATH . '/Combine-2.jpg',
758                'command' => $imageProcessor->IM_commands,
759            ];
760        } else {
761            $result = [
762                'status' => [$this->imageGenerationFailedMessage()],
763                'command' => $imageProcessor->IM_commands,
764            ];
765        }
766        return $this->getImageTestResponse($result);
767    }
768
769    /**
770     * GD with simple box
771     *
772     * @return ResponseInterface
773     */
774    public function imageProcessingGdlibSimpleAction(): ResponseInterface
775    {
776        $imageProcessor = $this->initializeImageProcessor();
777        $gifOrPng = $imageProcessor->gifExtension;
778        $image = imagecreatetruecolor(300, 225);
779        $backgroundColor = imagecolorallocate($image, 0, 0, 0);
780        imagefilledrectangle($image, 0, 0, 300, 225, $backgroundColor);
781        $workArea = [0, 0, 300, 225];
782        $conf = [
783            'dimensions' => '10,50,280,50',
784            'color' => 'olive',
785        ];
786        $imageProcessor->makeBox($image, $conf, $workArea);
787        $outputFile = $this->getImagesPath() . $imageProcessor->filenamePrefix . StringUtility::getUniqueId('gdSimple') . '.' . $gifOrPng;
788        $imageProcessor->ImageWrite($image, $outputFile);
789        $imResult = $imageProcessor->getImageDimensions($outputFile);
790        $result = [
791            'fileExists' => true,
792            'outputFile' => $imResult[3],
793            'referenceFile' => self::TEST_REFERENCE_PATH . '/Gdlib-simple.' . $gifOrPng,
794            'command' => $imageProcessor->IM_commands,
795        ];
796        return $this->getImageTestResponse($result);
797    }
798
799    /**
800     * GD from image with box
801     *
802     * @return ResponseInterface
803     */
804    public function imageProcessingGdlibFromFileAction(): ResponseInterface
805    {
806        $imageProcessor = $this->initializeImageProcessor();
807        $gifOrPng = $imageProcessor->gifExtension;
808        $imageBasePath = ExtensionManagementUtility::extPath('install') . 'Resources/Public/Images/';
809        $inputFile = $imageBasePath . 'TestInput/Test.' . $gifOrPng;
810        $image = $imageProcessor->imageCreateFromFile($inputFile);
811        $workArea = [0, 0, 400, 300];
812        $conf = [
813            'dimensions' => '10,50,380,50',
814            'color' => 'olive',
815        ];
816        $imageProcessor->makeBox($image, $conf, $workArea);
817        $outputFile = $this->getImagesPath() . $imageProcessor->filenamePrefix . StringUtility::getUniqueId('gdBox') . '.' . $gifOrPng;
818        $imageProcessor->ImageWrite($image, $outputFile);
819        $imResult = $imageProcessor->getImageDimensions($outputFile);
820        $result = [
821            'fileExists' => true,
822            'outputFile' => $imResult[3],
823            'referenceFile' => self::TEST_REFERENCE_PATH . '/Gdlib-box.' . $gifOrPng,
824            'command' => $imageProcessor->IM_commands,
825        ];
826        return $this->getImageTestResponse($result);
827    }
828
829    /**
830     * GD with text
831     *
832     * @return ResponseInterface
833     */
834    public function imageProcessingGdlibRenderTextAction(): ResponseInterface
835    {
836        $imageProcessor = $this->initializeImageProcessor();
837        $gifOrPng = $imageProcessor->gifExtension;
838        $image = imagecreatetruecolor(300, 225);
839        $backgroundColor = imagecolorallocate($image, 128, 128, 150);
840        imagefilledrectangle($image, 0, 0, 300, 225, $backgroundColor);
841        $workArea = [0, 0, 300, 225];
842        $conf = [
843            'iterations' => 1,
844            'angle' => 0,
845            'antiAlias' => 1,
846            'text' => 'HELLO WORLD',
847            'fontColor' => '#003366',
848            'fontSize' => 30,
849            'fontFile' => ExtensionManagementUtility::extPath('install') . 'Resources/Private/Font/vera.ttf',
850            'offset' => '30,80',
851        ];
852        $conf['BBOX'] = $imageProcessor->calcBBox($conf);
853        $imageProcessor->makeText($image, $conf, $workArea);
854        $outputFile = $this->getImagesPath() . $imageProcessor->filenamePrefix . StringUtility::getUniqueId('gdText') . '.' . $gifOrPng;
855        $imageProcessor->ImageWrite($image, $outputFile);
856        $imResult = $imageProcessor->getImageDimensions($outputFile);
857        $result = [
858            'fileExists' => true,
859            'outputFile' => $imResult[3],
860            'referenceFile' => self::TEST_REFERENCE_PATH . '/Gdlib-text.' . $gifOrPng,
861            'command' => $imageProcessor->IM_commands,
862        ];
863        return $this->getImageTestResponse($result);
864    }
865
866    /**
867     * GD with text, niceText
868     *
869     * @return ResponseInterface
870     */
871    public function imageProcessingGdlibNiceTextAction(): ResponseInterface
872    {
873        if (!$this->isImageMagickEnabledAndConfigured()) {
874            return new JsonResponse([
875                'status' => [$this->imageMagickDisabledMessage()],
876            ]);
877        }
878        $imageProcessor = $this->initializeImageProcessor();
879        $gifOrPng = $imageProcessor->gifExtension;
880        $image = imagecreatetruecolor(300, 225);
881        $backgroundColor = imagecolorallocate($image, 128, 128, 150);
882        imagefilledrectangle($image, 0, 0, 300, 225, $backgroundColor);
883        $workArea = [0, 0, 300, 225];
884        $conf = [
885            'iterations' => 1,
886            'angle' => 0,
887            'antiAlias' => 1,
888            'text' => 'HELLO WORLD',
889            'fontColor' => '#003366',
890            'fontSize' => 30,
891            'fontFile' => ExtensionManagementUtility::extPath('install') . 'Resources/Private/Font/vera.ttf',
892            'offset' => '30,80',
893        ];
894        $conf['BBOX'] = $imageProcessor->calcBBox($conf);
895        $imageProcessor->makeText($image, $conf, $workArea);
896        $outputFile = $this->getImagesPath() . $imageProcessor->filenamePrefix . StringUtility::getUniqueId('gdText') . '.' . $gifOrPng;
897        $imageProcessor->ImageWrite($image, $outputFile);
898        $conf['offset'] = '30,120';
899        $conf['niceText'] = 1;
900        $imageProcessor->makeText($image, $conf, $workArea);
901        $outputFile = $this->getImagesPath() . $imageProcessor->filenamePrefix . StringUtility::getUniqueId('gdNiceText') . '.' . $gifOrPng;
902        $imageProcessor->ImageWrite($image, $outputFile);
903        $imResult = $imageProcessor->getImageDimensions($outputFile);
904        $result = [
905            'fileExists' => true,
906            'outputFile' => $imResult[3],
907            'referenceFile' => self::TEST_REFERENCE_PATH . '/Gdlib-niceText.' . $gifOrPng,
908            'command' => $imageProcessor->IM_commands,
909        ];
910        return $this->getImageTestResponse($result);
911    }
912
913    /**
914     * GD with text, niceText, shadow
915     *
916     * @return ResponseInterface
917     */
918    public function imageProcessingGdlibNiceTextShadowAction(): ResponseInterface
919    {
920        if (!$this->isImageMagickEnabledAndConfigured()) {
921            return new JsonResponse([
922                'status' => [$this->imageMagickDisabledMessage()],
923            ]);
924        }
925        $imageProcessor = $this->initializeImageProcessor();
926        $gifOrPng = $imageProcessor->gifExtension;
927        $image = imagecreatetruecolor(300, 225);
928        $backgroundColor = imagecolorallocate($image, 128, 128, 150);
929        imagefilledrectangle($image, 0, 0, 300, 225, $backgroundColor);
930        $workArea = [0, 0, 300, 225];
931        $conf = [
932            'iterations' => 1,
933            'angle' => 0,
934            'antiAlias' => 1,
935            'text' => 'HELLO WORLD',
936            'fontColor' => '#003366',
937            'fontSize' => 30,
938            'fontFile' => ExtensionManagementUtility::extPath('install') . 'Resources/Private/Font/vera.ttf',
939            'offset' => '30,80',
940        ];
941        $conf['BBOX'] = $imageProcessor->calcBBox($conf);
942        $imageProcessor->makeText($image, $conf, $workArea);
943        $outputFile = $this->getImagesPath() . $imageProcessor->filenamePrefix . StringUtility::getUniqueId('gdText') . '.' . $gifOrPng;
944        $imageProcessor->ImageWrite($image, $outputFile);
945        $conf['offset'] = '30,120';
946        $conf['niceText'] = 1;
947        $imageProcessor->makeText($image, $conf, $workArea);
948        $outputFile = $this->getImagesPath() . $imageProcessor->filenamePrefix . StringUtility::getUniqueId('gdNiceText') . '.' . $gifOrPng;
949        $imageProcessor->ImageWrite($image, $outputFile);
950        $conf['offset'] = '30,160';
951        $conf['niceText'] = 1;
952        $conf['shadow.'] = [
953            'offset' => '2,2',
954            'blur' => '20',
955            'opacity' => '50',
956            'color' => 'black',
957        ];
958        // Warning: Re-uses $image from above!
959        $imageProcessor->makeShadow($image, $conf['shadow.'], $workArea, $conf);
960        $imageProcessor->makeText($image, $conf, $workArea);
961        $outputFile = $this->getImagesPath() . $imageProcessor->filenamePrefix . StringUtility::getUniqueId('GDwithText-niceText-shadow') . '.' . $gifOrPng;
962        $imageProcessor->ImageWrite($image, $outputFile);
963        $imResult = $imageProcessor->getImageDimensions($outputFile);
964        $result = [
965            'fileExists' => true,
966            'outputFile' => $imResult[3],
967            'referenceFile' => self::TEST_REFERENCE_PATH . '/Gdlib-shadow.' . $gifOrPng,
968            'command' => $imageProcessor->IM_commands,
969        ];
970        return $this->getImageTestResponse($result);
971    }
972
973    /**
974     * Initialize image processor
975     *
976     * @return GraphicalFunctions Initialized image processor
977     */
978    protected function initializeImageProcessor(): GraphicalFunctions
979    {
980        $imageProcessor = GeneralUtility::makeInstance(GraphicalFunctions::class);
981        $imageProcessor->dontCheckForExistingTempFile = true;
982        $imageProcessor->filenamePrefix = 'installTool-';
983        $imageProcessor->dontCompress = true;
984        $imageProcessor->alternativeOutputKey = 'typo3InstallTest';
985        $imageProcessor->setImageFileExt(self::IMAGE_FILE_EXT);
986        return $imageProcessor;
987    }
988
989    /**
990     * Determine ImageMagick / GraphicsMagick version
991     *
992     * @return string Version
993     */
994    protected function determineImageMagickVersion(): string
995    {
996        $command = CommandUtility::imageMagickCommand('identify', '-version');
997        CommandUtility::exec($command, $result);
998        $string = $result[0] ?? '';
999        $version = '';
1000        if (!empty($string)) {
1001            [, $version] = explode('Magick', $string);
1002            [$version] = explode(' ', trim($version));
1003            $version = trim($version);
1004        }
1005        return $version;
1006    }
1007
1008    /**
1009     * Convert to jpg from given input format
1010     *
1011     * @param string $inputFormat
1012     * @return ResponseInterface
1013     */
1014    protected function convertImageFormatsToJpg(string $inputFormat): ResponseInterface
1015    {
1016        if (!$this->isImageMagickEnabledAndConfigured()) {
1017            return new JsonResponse([
1018                'status' => [$this->imageMagickDisabledMessage()],
1019            ]);
1020        }
1021        if (!GeneralUtility::inList($GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'], $inputFormat)) {
1022            return new JsonResponse([
1023                'status' => [
1024                    new FlashMessage(
1025                        'Handling format ' . $inputFormat . ' must be enabled in TYPO3_CONF_VARS[\'GFX\'][\'imagefile_ext\']',
1026                        'Skipped test',
1027                        FlashMessage::WARNING
1028                    ),
1029                ],
1030            ]);
1031        }
1032        $imageBasePath = ExtensionManagementUtility::extPath('install') . 'Resources/Public/Images/';
1033        $imageProcessor = $this->initializeImageProcessor();
1034        $inputFile = $imageBasePath . 'TestInput/Test.' . $inputFormat;
1035        $imageProcessor->imageMagickConvert_forceFileNameBody = StringUtility::getUniqueId('read') . '-' . $inputFormat;
1036        $imResult = $imageProcessor->imageMagickConvert($inputFile, 'jpg', '300', '', '', '', [], true);
1037        if ($imResult !== null) {
1038            $result = [
1039                'fileExists' => file_exists($imResult[3]),
1040                'outputFile' => $imResult[3],
1041                'referenceFile' => self::TEST_REFERENCE_PATH . '/Read-' . $inputFormat . '.jpg',
1042                'command' => $imageProcessor->IM_commands,
1043            ];
1044        } else {
1045            $result = [
1046                'status' => [$this->imageGenerationFailedMessage()],
1047                'command' => $imageProcessor->IM_commands,
1048            ];
1049        }
1050        return $this->getImageTestResponse($result);
1051    }
1052
1053    /**
1054     * Get details about all configured database connections
1055     *
1056     * @return array
1057     */
1058    protected function getDatabaseConnectionInformation(): array
1059    {
1060        $connectionInfos = [];
1061        $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
1062        foreach ($connectionPool->getConnectionNames() as $connectionName) {
1063            $connection = $connectionPool->getConnectionByName($connectionName);
1064            $connectionParameters = $connection->getParams();
1065            $connectionInfo = [
1066                'connectionName' => $connectionName,
1067                'version' => $connection->getServerVersion(),
1068                'databaseName' => $connection->getDatabase(),
1069                'username' => $connectionParameters['user'] ?? '',
1070                'host' => $connectionParameters['host'] ?? '',
1071                'port' => $connectionParameters['port'] ?? '',
1072                'socket' => $connectionParameters['unix_socket'] ?? '',
1073                'numberOfTables' => count($connection->createSchemaManager()->listTableNames()),
1074                'numberOfMappedTables' => 0,
1075            ];
1076            if (isset($GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'])
1077                && is_array($GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'])
1078            ) {
1079                // Count number of array keys having $connectionName as value
1080                $connectionInfo['numberOfMappedTables'] = count(array_intersect(
1081                    $GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'],
1082                    [$connectionName]
1083                ));
1084            }
1085            $connectionInfos[] = $connectionInfo;
1086        }
1087        return $connectionInfos;
1088    }
1089
1090    /**
1091     * Get details about the application context
1092     *
1093     * @return array
1094     */
1095    protected function getApplicationContextInformation(): array
1096    {
1097        $applicationContext = Environment::getContext();
1098        $status = $applicationContext->isProduction() ? InformationStatus::STATUS_OK : InformationStatus::STATUS_WARNING;
1099
1100        return [
1101            'context' => (string)$applicationContext,
1102            'status' => $status,
1103        ];
1104    }
1105
1106    /**
1107     * Get sender address from configuration
1108     * ['TYPO3_CONF_VARS']['MAIL']['defaultMailFromAddress']
1109     * If this setting is empty fall back to 'no-reply@example.com'
1110     *
1111     * @return string Returns an email address
1112     */
1113    protected function getSenderEmailAddress(): string
1114    {
1115        return !empty($GLOBALS['TYPO3_CONF_VARS']['MAIL']['defaultMailFromAddress'])
1116            ? $GLOBALS['TYPO3_CONF_VARS']['MAIL']['defaultMailFromAddress']
1117            : 'no-reply@example.com';
1118    }
1119
1120    /**
1121     * Gets sender name from configuration
1122     * ['TYPO3_CONF_VARS']['MAIL']['defaultMailFromName']
1123     * If this setting is empty, it falls back to a default string.
1124     *
1125     * @return string
1126     */
1127    protected function getSenderEmailName(): string
1128    {
1129        return !empty($GLOBALS['TYPO3_CONF_VARS']['MAIL']['defaultMailFromName'])
1130            ? $GLOBALS['TYPO3_CONF_VARS']['MAIL']['defaultMailFromName']
1131            : 'TYPO3 CMS install tool';
1132    }
1133
1134    /**
1135     * Gets email subject from configuration
1136     * ['TYPO3_CONF_VARS']['SYS']['sitename']
1137     * If this setting is empty, it falls back to a default string.
1138     *
1139     * @return string
1140     */
1141    protected function getEmailSubject(): string
1142    {
1143        $name = !empty($GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'])
1144            ? ' from site "' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] . '"'
1145            : '';
1146        return 'Test TYPO3 CMS mail delivery' . $name;
1147    }
1148
1149    /**
1150     * Create a JsonResponse from single image tests
1151     *
1152     * @param array $testResult
1153     * @return ResponseInterface
1154     */
1155    protected function getImageTestResponse(array $testResult): ResponseInterface
1156    {
1157        $responseData = [
1158            'success' => true,
1159        ];
1160        foreach ($testResult as $resultKey => $value) {
1161            if ($resultKey === 'referenceFile' && !empty($testResult['referenceFile'])) {
1162                $referenceFileArray = explode('.', $testResult['referenceFile']);
1163                $fileExt = end($referenceFileArray);
1164                $responseData['referenceFile'] = 'data:image/' . $fileExt . ';base64,' . base64_encode((string)file_get_contents($testResult['referenceFile']));
1165            } elseif ($resultKey === 'outputFile' && !empty($testResult['outputFile'])) {
1166                $outputFileArray = explode('.', $testResult['outputFile']);
1167                $fileExt = end($outputFileArray);
1168                $responseData['outputFile'] = 'data:image/' . $fileExt . ';base64,' . base64_encode((string)file_get_contents($testResult['outputFile']));
1169            } else {
1170                $responseData[$resultKey] = $value;
1171            }
1172        }
1173        return new JsonResponse($responseData);
1174    }
1175
1176    /**
1177     * Create a 'image generation failed' message
1178     *
1179     * @return FlashMessage
1180     */
1181    protected function imageGenerationFailedMessage(): FlashMessage
1182    {
1183        return new FlashMessage(
1184            'ImageMagick / GraphicsMagick handling is enabled, but the execute'
1185            . ' command returned an error. Please check your settings, especially'
1186            . ' [\'GFX\'][\'processor_path\'] and [\'GFX\'][\'processor_path_lzw\'] and ensure Ghostscript is installed on your server.',
1187            'Image generation failed',
1188            FlashMessage::ERROR
1189        );
1190    }
1191
1192    /**
1193     * Find out if ImageMagick or GraphicsMagick is enabled and set up
1194     *
1195     * @return bool TRUE if enabled and path is set
1196     */
1197    protected function isImageMagickEnabledAndConfigured(): bool
1198    {
1199        $enabled = $GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_enabled'];
1200        $path = $GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_path'];
1201        return $enabled && $path;
1202    }
1203
1204    /**
1205     * Create a 'imageMagick disabled' message
1206     *
1207     * @return FlashMessage
1208     */
1209    protected function imageMagickDisabledMessage(): FlashMessage
1210    {
1211        return new FlashMessage(
1212            'ImageMagick / GraphicsMagick handling is disabled or not configured correctly.',
1213            'Tests not executed',
1214            FlashMessage::ERROR
1215        );
1216    }
1217
1218    /**
1219     * Return the temp image dir.
1220     * If not exist it will be created
1221     *
1222     * @return string
1223     */
1224    protected function getImagesPath(): string
1225    {
1226        $imagePath = Environment::getPublicPath() . '/typo3temp/assets/images/';
1227        if (!is_dir($imagePath)) {
1228            GeneralUtility::mkdir_deep($imagePath);
1229        }
1230        return $imagePath;
1231    }
1232}
1233