Passed
Push — master ( 3fa3dd...e859ce )
by
unknown
17:13
created

ServerResponseCheck::wrapValues()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 4
c 0
b 0
f 0
nc 1
nop 3
dl 0
loc 7
rs 10
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
18
namespace TYPO3\CMS\Install\SystemEnvironment\ServerResponse;
19
20
use GuzzleHttp\Client;
21
use GuzzleHttp\Exception\BadResponseException;
22
use function GuzzleHttp\Promise\settle;
23
use Psr\Http\Message\ResponseInterface;
24
use TYPO3\CMS\Core\Messaging\FlashMessage;
25
use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
26
use TYPO3\CMS\Core\Utility\GeneralUtility;
27
use TYPO3\CMS\Install\SystemEnvironment\CheckInterface;
28
use TYPO3\CMS\Reports\Status;
29
30
/**
31
 * Checks how use web server is interpreting static files concerning
32
 * their `content-type` and evaluated content in HTTP responses.
33
 *
34
 * @internal should only be used from within TYPO3 Core
35
 */
36
class ServerResponseCheck implements CheckInterface
37
{
38
    protected const WRAP_FLAT = 1;
39
    protected const WRAP_NESTED = 2;
40
41
    /**
42
     * @var bool
43
     */
44
    protected $useMarkup;
45
46
    /**
47
     * @var FlashMessageQueue
48
     */
49
    protected $messageQueue;
50
51
    /**
52
     * @var FileLocation
53
     */
54
    protected $assetLocation;
55
56
    /**
57
     * @var FileDeclaration[]
58
     */
59
    protected $fileDeclarations;
60
61
    public function __construct(bool $useMarkup = true)
62
    {
63
        $this->useMarkup = $useMarkup;
64
65
        $fileName = bin2hex(random_bytes(4));
66
        $folderName = bin2hex(random_bytes(4));
67
        $this->assetLocation = new FileLocation(sprintf('/typo3temp/assets/%s.tmp/', $folderName));
68
        $this->fileDeclarations = $this->initializeFileDeclarations($fileName);
69
    }
70
71
    public function asStatus(): Status
72
    {
73
        $messageQueue = $this->getStatus();
74
        $messages = [];
75
        foreach ($messageQueue->getAllMessages() as $flashMessage) {
76
            $messages[] = $flashMessage->getMessage();
77
        }
78
        if ($messageQueue->getAllMessages(FlashMessage::ERROR) !== []) {
79
            $title = 'Potential vulnerabilities';
80
            $severity = Status::ERROR;
81
        } elseif ($messageQueue->getAllMessages(FlashMessage::WARNING) !== []) {
82
            $title = 'Warnings';
83
            $severity = Status::WARNING;
84
        }
85
        return new Status(
86
            'Server Response on static files',
87
            $title ?? 'OK',
88
            $this->wrapList($messages, '', self::WRAP_NESTED),
89
            $severity ?? Status::OK
90
        );
91
    }
92
93
    public function getStatus(): FlashMessageQueue
94
    {
95
        $messageQueue = new FlashMessageQueue('install-server-response-check');
96
        if (PHP_SAPI === 'cli-server') {
97
            $messageQueue->addMessage(
98
                new FlashMessage(
99
                    'Skipped for PHP_SAPI=cli-server',
100
                    'Checks skipped',
101
                    FlashMessage::WARNING
102
                )
103
            );
104
            return $messageQueue;
105
        }
106
        try {
107
            $this->buildFileDeclarations();
108
            $this->processFileDeclarations($messageQueue);
109
            $this->finishMessageQueue($messageQueue);
110
        } finally {
111
            $this->purgeFileDeclarations();
112
        }
113
        return $messageQueue;
114
    }
115
116
    protected function initializeFileDeclarations(string $fileName): array
117
    {
118
        return [
119
            (new FileDeclaration($this->assetLocation, $fileName . '.html'))
120
                ->withExpectedContentType('text/html')
121
                ->withExpectedContent('HTML content'),
122
            (new FileDeclaration($this->assetLocation, $fileName . '.wrong'))
123
                ->withUnexpectedContentType('text/html')
124
                ->withExpectedContent('HTML content'),
125
            (new FileDeclaration($this->assetLocation, $fileName . '.html.wrong'))
126
                ->withUnexpectedContentType('text/html')
127
                ->withExpectedContent('HTML content'),
128
            (new FileDeclaration($this->assetLocation, $fileName . '.1.svg.wrong'))
129
                ->withBuildFlags(FileDeclaration::FLAG_BUILD_SVG | FileDeclaration::FLAG_BUILD_SVG_DOCUMENT)
130
                ->withUnexpectedContentType('image/svg+xml')
131
                ->withExpectedContent('SVG content'),
132
            (new FileDeclaration($this->assetLocation, $fileName . '.2.svg.wrong'))
133
                ->withBuildFlags(FileDeclaration::FLAG_BUILD_SVG | FileDeclaration::FLAG_BUILD_SVG_DOCUMENT)
134
                ->withUnexpectedContentType('image/svg')
135
                ->withExpectedContent('SVG content'),
136
            (new FileDeclaration($this->assetLocation, $fileName . '.php.wrong', true))
137
                ->withBuildFlags(FileDeclaration::FLAG_BUILD_PHP | FileDeclaration::FLAG_BUILD_HTML_DOCUMENT)
138
                ->withUnexpectedContent('PHP content'),
139
            (new FileDeclaration($this->assetLocation, $fileName . '.html.txt'))
140
                ->withExpectedContentType('text/plain')
141
                ->withUnexpectedContentType('text/html')
142
                ->withExpectedContent('HTML content'),
143
            (new FileDeclaration($this->assetLocation, $fileName . '.php.txt', true))
144
                ->withBuildFlags(FileDeclaration::FLAG_BUILD_PHP | FileDeclaration::FLAG_BUILD_HTML_DOCUMENT)
145
                ->withUnexpectedContent('PHP content'),
146
        ];
147
    }
148
149
    protected function buildFileDeclarations(): void
150
    {
151
        foreach ($this->fileDeclarations as $fileDeclaration) {
152
            $filePath = $fileDeclaration->getFileLocation()->getFilePath();
153
            if (!is_dir($filePath)) {
154
                GeneralUtility::mkdir_deep($filePath);
155
            }
156
            file_put_contents(
157
                $filePath . $fileDeclaration->getFileName(),
158
                $fileDeclaration->buildContent()
159
            );
160
        }
161
    }
162
163
    protected function purgeFileDeclarations(): void
164
    {
165
        GeneralUtility::rmdir($this->assetLocation->getFilePath(), true);
166
    }
167
168
    protected function processFileDeclarations(FlashMessageQueue $messageQueue): void
169
    {
170
        $promises = [];
171
        $client = new Client(['timeout' => 10]);
172
        foreach ($this->fileDeclarations as $fileDeclaration) {
173
            $promises[] = $client->requestAsync('GET', $fileDeclaration->getUrl());
174
        }
175
        foreach (settle($promises)->wait() as $index => $response) {
176
            $fileDeclaration = $this->fileDeclarations[$index];
177
            if (($response['reason'] ?? null) instanceof BadResponseException) {
178
                $messageQueue->addMessage(
179
                    new FlashMessage(
180
                        sprintf(
181
                            '(%d): %s',
182
                            $response['reason']->getCode(),
183
                            $response['reason']->getRequest()->getUri()
184
                        ),
185
                        'HTTP warning',
186
                        FlashMessage::WARNING
187
                    )
188
                );
189
                continue;
190
            }
191
            if (!($response['value'] ?? null) instanceof ResponseInterface || $fileDeclaration->matches($response['value'])) {
192
                continue;
193
            }
194
            $messageQueue->addMessage(
195
                new FlashMessage(
196
                    $this->createMismatchMessage($fileDeclaration, $response['value']),
197
                    'Unexpected server response',
198
                    $fileDeclaration->shallFail() ? FlashMessage::ERROR : FlashMessage::WARNING
199
                )
200
            );
201
        }
202
    }
203
204
    protected function finishMessageQueue(FlashMessageQueue $messageQueue): void
205
    {
206
        if ($messageQueue->getAllMessages(FlashMessage::WARNING) !== []
207
            || $messageQueue->getAllMessages(FlashMessage::ERROR) !== []) {
208
            return;
209
        }
210
        $messageQueue->addMessage(
211
            new FlashMessage(
212
                sprintf('All %d files processed correctly', count($this->fileDeclarations)),
213
                'Expected server response',
214
                FlashMessage::OK
215
            )
216
        );
217
    }
218
219
    protected function createMismatchMessage(FileDeclaration $fileDeclaration, ResponseInterface $response): string
220
    {
221
        $messageParts = array_map(
222
            function (StatusMessage $mismatch): string {
223
                return vsprintf(
224
                    $mismatch->getMessage(),
225
                    $this->wrapValues($mismatch->getValues(), '<code>', '</code>')
226
                );
227
            },
228
            $fileDeclaration->getMismatches($response)
229
        );
230
        return $this->wrapList($messageParts, $fileDeclaration->getUrl(), self::WRAP_FLAT);
231
    }
232
233
    protected function wrapList(array $items, string $label, int $style): string
234
    {
235
        if (!$this->useMarkup) {
236
            return sprintf(
237
                '%s%s',
238
                $label ? $label . ': ' : '',
239
                implode(', ', $items)
240
            );
241
        }
242
        if ($style === self::WRAP_NESTED) {
243
            return sprintf(
244
                '%s<ul>%s</ul>',
245
                $label,
246
                implode('', $this->wrapItems($items, '<li>', '</li>'))
247
            );
248
        }
249
        return sprintf(
250
            '<p>%s%s</p>',
251
            $label,
252
            implode('', $this->wrapItems($items, '<br>', ''))
253
        );
254
    }
255
256
    protected function wrapItems(array $items, string $before, string $after): array
257
    {
258
        return array_map(
259
            function (string $item) use ($before, $after): string {
260
                return $before . $item . $after;
261
            },
262
            array_filter($items)
263
        );
264
    }
265
266
    protected function wrapValues(array $values, string $before, string $after): array
267
    {
268
        return array_map(
269
            function (string $value) use ($before, $after): string {
270
                return $this->wrapValue($value, $before, $after);
271
            },
272
            array_filter($values)
273
        );
274
    }
275
276
    protected function wrapValue(string $value, string $before, string $after): string
277
    {
278
        if ($this->useMarkup) {
279
            return $before . htmlspecialchars($value) . $after;
280
        }
281
        return $value;
282
    }
283
}
284