Completed
Push — master ( f34eb5...dcac1c )
by
unknown
16:13
created

ServerResponseCheck::processFileDeclarations()   B

Complexity

Conditions 7
Paths 8

Size

Total Lines 31
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 7
eloc 23
nc 8
nop 1
dl 0
loc 31
rs 8.6186
c 1
b 0
f 0
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\Core\Environment;
25
use TYPO3\CMS\Core\Messaging\FlashMessage;
26
use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
27
use TYPO3\CMS\Core\Utility\GeneralUtility;
28
use TYPO3\CMS\Core\Utility\PathUtility;
29
use TYPO3\CMS\Install\SystemEnvironment\CheckInterface;
30
use TYPO3\CMS\Reports\Status;
31
32
/**
33
 * Checks how use web server is interpreting static files concerning
34
 * their `content-type` and evaluated content in HTTP responses.
35
 *
36
 * @internal should only be used from within TYPO3 Core
37
 */
38
class ServerResponseCheck implements CheckInterface
39
{
40
    /**
41
     * @var bool
42
     */
43
    protected $useMarkup;
44
45
    /**
46
     * @var FlashMessageQueue
47
     */
48
    protected $messageQueue;
49
50
    /**
51
     * @var string
52
     */
53
    protected $filePath;
54
55
    /**
56
     * @var string
57
     */
58
    protected $baseUrl;
59
60
    /**
61
     * @var FileDeclaration[]
62
     */
63
    protected $fileDeclarations;
64
65
    public function __construct(bool $useMarkup = true)
66
    {
67
        $this->useMarkup = $useMarkup;
68
69
        $fileName = bin2hex(random_bytes(4));
70
        $folderName = bin2hex(random_bytes(4));
71
        $this->filePath = Environment::getPublicPath()
72
            . sprintf('/typo3temp/assets/%s.tmp/', $folderName);
73
        $this->baseUrl = GeneralUtility::getIndpEnv('TYPO3_REQUEST_HOST')
74
            . PathUtility::getAbsoluteWebPath($this->filePath);
75
        $this->fileDeclarations = $this->initializeFileDeclarations($fileName);
76
    }
77
78
    public function asStatus(): Status
79
    {
80
        $messageQueue = $this->getStatus();
81
        $messages = [];
82
        foreach ($messageQueue->getAllMessages() as $flashMessage) {
83
            $messages[] = $flashMessage->getMessage();
84
        }
85
        if ($messageQueue->getAllMessages(FlashMessage::ERROR) !== []) {
86
            $title = 'Potential vulnerabilities';
87
            $severity = Status::ERROR;
88
        } elseif ($messageQueue->getAllMessages(FlashMessage::WARNING) !== []) {
89
            $title = 'Warnings';
90
            $severity = Status::WARNING;
91
        }
92
        return new Status(
93
            'Server Response on static files',
94
            $title ?? 'OK',
95
            $this->wrapList($messages),
96
            $severity ?? Status::OK
97
        );
98
    }
99
100
    public function getStatus(): FlashMessageQueue
101
    {
102
        $messageQueue = new FlashMessageQueue('install-server-response-check');
103
        if (PHP_SAPI === 'cli-server') {
104
            $messageQueue->addMessage(
105
                new FlashMessage(
106
                    'Skipped for PHP_SAPI=cli-server',
107
                    'Checks skipped',
108
                    FlashMessage::WARNING
109
                )
110
            );
111
            return $messageQueue;
112
        }
113
        try {
114
            $this->buildFileDeclarations();
115
            $this->processFileDeclarations($messageQueue);
116
            $this->finishMessageQueue($messageQueue);
117
        } finally {
118
            $this->purgeFileDeclarations();
119
        }
120
        return $messageQueue;
121
    }
122
123
    protected function initializeFileDeclarations(string $fileName): array
124
    {
125
        return [
126
            (new FileDeclaration($fileName . '.html'))
127
                ->withExpectedContentType('text/html')
128
                ->withExpectedContent('HTML content'),
129
            (new FileDeclaration($fileName . '.wrong'))
130
                ->withUnexpectedContentType('text/html')
131
                ->withExpectedContent('HTML content'),
132
            (new FileDeclaration($fileName . '.html.wrong'))
133
                ->withUnexpectedContentType('text/html')
134
                ->withExpectedContent('HTML content'),
135
            (new FileDeclaration($fileName . '.1.svg.wrong'))
136
                ->withBuildFlags(FileDeclaration::FLAG_BUILD_SVG | FileDeclaration::FLAG_BUILD_SVG_DOCUMENT)
137
                ->withUnexpectedContentType('image/svg+xml')
138
                ->withExpectedContent('SVG content'),
139
            (new FileDeclaration($fileName . '.2.svg.wrong'))
140
                ->withBuildFlags(FileDeclaration::FLAG_BUILD_SVG | FileDeclaration::FLAG_BUILD_SVG_DOCUMENT)
141
                ->withUnexpectedContentType('image/svg')
142
                ->withExpectedContent('SVG content'),
143
            (new FileDeclaration($fileName . '.php.wrong', true))
144
                ->withBuildFlags(FileDeclaration::FLAG_BUILD_PHP | FileDeclaration::FLAG_BUILD_HTML_DOCUMENT)
145
                ->withUnexpectedContent('PHP content'),
146
            (new FileDeclaration($fileName . '.html.txt'))
147
                ->withExpectedContentType('text/plain')
148
                ->withUnexpectedContentType('text/html')
149
                ->withExpectedContent('HTML content'),
150
            (new FileDeclaration($fileName . '.php.txt', true))
151
                ->withBuildFlags(FileDeclaration::FLAG_BUILD_PHP | FileDeclaration::FLAG_BUILD_HTML_DOCUMENT)
152
                ->withUnexpectedContent('PHP content'),
153
        ];
154
    }
155
156
    protected function buildFileDeclarations(): void
157
    {
158
        if (!is_dir($this->filePath)) {
159
            GeneralUtility::mkdir_deep($this->filePath);
160
        }
161
        foreach ($this->fileDeclarations as $fileDeclaration) {
162
            file_put_contents(
163
                $this->filePath . $fileDeclaration->getFileName(),
164
                $fileDeclaration->buildContent()
165
            );
166
        }
167
    }
168
169
    protected function purgeFileDeclarations(): void
170
    {
171
        GeneralUtility::rmdir($this->filePath, true);
172
    }
173
174
    protected function processFileDeclarations(FlashMessageQueue $messageQueue): void
175
    {
176
        $promises = [];
177
        $client = new Client(['base_uri' => $this->baseUrl]);
178
        foreach ($this->fileDeclarations as $fileDeclaration) {
179
            $promises[] = $client->requestAsync('GET', $fileDeclaration->getFileName());
180
        }
181
        foreach (settle($promises)->wait() as $index => $response) {
182
            $fileDeclaration = $this->fileDeclarations[$index];
183
            if (($response['reason'] ?? null) instanceof BadResponseException) {
184
                $messageQueue->addMessage(
185
                    new FlashMessage(
186
                        sprintf(
187
                            '(%d): %s',
188
                            $response['reason']->getCode(),
189
                            $response['reason']->getRequest()->getUri()
190
                        ),
191
                        'HTTP warning',
192
                        FlashMessage::WARNING
193
                    )
194
                );
195
                continue;
196
            }
197
            if (!($response['value'] ?? null) instanceof ResponseInterface || $fileDeclaration->matches($response['value'])) {
198
                continue;
199
            }
200
            $messageQueue->addMessage(
201
                new FlashMessage(
202
                    $this->createMismatchMessage($fileDeclaration, $response['value']),
203
                    'Unexpected server response',
204
                    $fileDeclaration->shallFail() ? FlashMessage::ERROR : FlashMessage::WARNING
205
                )
206
            );
207
        }
208
    }
209
210
    protected function finishMessageQueue(FlashMessageQueue $messageQueue): void
211
    {
212
        if ($messageQueue->getAllMessages(FlashMessage::WARNING) !== []
213
            || $messageQueue->getAllMessages(FlashMessage::ERROR) !== []) {
214
            return;
215
        }
216
        $messageQueue->addMessage(
217
            new FlashMessage(
218
                sprintf('All %d files processed correctly', count($this->fileDeclarations)),
219
                'Expected server response',
220
                FlashMessage::OK
221
            )
222
        );
223
    }
224
225
    protected function createMismatchMessage(FileDeclaration $fileDeclaration, ResponseInterface $response): string
226
    {
227
        $messageParts = [];
228
        $mismatches = $fileDeclaration->getMismatches($response);
229
        if (in_array(FileDeclaration::MISMATCH_UNEXPECTED_CONTENT_TYPE, $mismatches, true)) {
230
            $messageParts[] = sprintf(
231
                'unexpected content-type %s',
232
                $this->wrapValue(
233
                    $fileDeclaration->getUnexpectedContentType(),
0 ignored issues
show
Bug introduced by
It seems like $fileDeclaration->getUnexpectedContentType() can also be of type null; however, parameter $value of TYPO3\CMS\Install\System...ponseCheck::wrapValue() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

233
                    /** @scrutinizer ignore-type */ $fileDeclaration->getUnexpectedContentType(),
Loading history...
234
                    '<code>',
235
                    '</code>'
236
                )
237
            );
238
        }
239
        if (in_array(FileDeclaration::MISMATCH_EXPECTED_CONTENT_TYPE, $mismatches, true)) {
240
            $messageParts[] = sprintf(
241
                'content-type mismatch %s, got %s',
242
                $this->wrapValue(
243
                    $fileDeclaration->getExpectedContent(),
244
                    '<code>',
245
                    '</code>'
246
                ),
247
                $this->wrapValue(
248
                    $response->getHeaderLine('content-type'),
249
                    '<code>',
250
                    '</code>'
251
                )
252
            );
253
        }
254
        if (in_array(FileDeclaration::MISMATCH_UNEXPECTED_CONTENT, $mismatches, true)) {
255
            $messageParts[] = sprintf(
256
                'unexpected content %s',
257
                $this->wrapValue(
258
                    $fileDeclaration->getUnexpectedContent(),
259
                    '<code>',
260
                    '</code>'
261
                )
262
            );
263
        }
264
        if (in_array(FileDeclaration::MISMATCH_EXPECTED_CONTENT, $mismatches, true)) {
265
            $messageParts[] = sprintf(
266
                'content mismatch %s',
267
                $this->wrapValue(
268
                    $fileDeclaration->getExpectedContent(),
269
                    '<code>',
270
                    '</code>'
271
                )
272
            );
273
        }
274
        return $this->wrapList(
275
            $messageParts,
276
            $this->baseUrl . $fileDeclaration->getFileName()
277
        );
278
    }
279
280
    protected function wrapList(array $items, string $label = ''): string
281
    {
282
        if ($this->useMarkup) {
283
            return sprintf(
284
                '%s<ul>%s</ul>',
285
                $label,
286
                implode('', $this->wrapItems($items, '<li>', '</li>'))
287
            );
288
        }
289
        return sprintf(
290
            '%s%s',
291
            $label ? $label . ': ' : '',
292
            implode(', ', $items)
293
        );
294
    }
295
296
    protected function wrapItems(array $items, string $before, string $after): array
297
    {
298
        return array_map(
299
            function (string $item) use ($before, $after): string {
300
                return $before . $item . $after;
301
            },
302
            array_filter($items)
303
        );
304
    }
305
306
    protected function wrapValue(string $value, string $before, string $after): string
307
    {
308
        if ($this->useMarkup) {
309
            return $before . htmlspecialchars($value) . $after;
310
        }
311
        return $value;
312
    }
313
}
314