• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

eliashaeussler / typo3-warming / 15509771334

07 Jun 2025 04:43PM UTC coverage: 86.535% (-4.1%) from 90.617%
15509771334

Pull #871

github

eliashaeussler
[FEATURE] Collect url metadata during cache warmup
Pull Request #871: [FEATURE] Collect url metadata during cache warmup

104 of 188 new or added lines in 9 files covered. (55.32%)

1 existing line in 1 file now uncovered.

1446 of 1671 relevant lines covered (86.54%)

9.25 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

49.25
/Classes/Http/Message/UrlMetadataFactory.php
1
<?php
2

3
declare(strict_types=1);
4

5
/*
6
 * This file is part of the TYPO3 CMS extension "warming".
7
 *
8
 * Copyright (C) 2021-2025 Elias Häußler <elias@haeussler.dev>
9
 *
10
 * This program is free software: you can redistribute it and/or modify
11
 * it under the terms of the GNU General Public License as published by
12
 * the Free Software Foundation, either version 2 of the License, or
13
 * (at your option) any later version.
14
 *
15
 * This program is distributed in the hope that it will be useful,
16
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18
 * GNU General Public License for more details.
19
 *
20
 * You should have received a copy of the GNU General Public License
21
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
22
 */
23

24
namespace EliasHaeussler\Typo3Warming\Http\Message;
25

26
use CuyZ\Valinor;
27
use Psr\Http\Message;
28
use Symfony\Component\DependencyInjection;
29
use TYPO3\CMS\Core;
30
use TYPO3\CMS\Extbase;
31

32
/**
33
 * UrlMetadataFactory
34
 *
35
 * @author Elias Häußler <elias@haeussler.dev>
36
 * @license GPL-2.0-or-later
37
 */
38
#[DependencyInjection\Attribute\Autoconfigure(public: true)]
39
final readonly class UrlMetadataFactory
40
{
41
    private const REQUEST_HEADER = 'X-Warming-Request-Id';
42
    private const RESPONSE_HEADER = 'X-Warming-Url-Metadata';
43

44
    private Valinor\Mapper\TreeMapper $mapper;
45

46
    public function __construct()
4✔
47
    {
48
        $this->mapper = $this->createMapper();
4✔
49
    }
50

51
    /**
52
     * Create url metadata object from authorized request.
53
     */
NEW
54
    public function createForRequest(Message\RequestInterface $request): ?UrlMetadata
×
55
    {
56
        // Early return if request header is missing (this usually happens on "normal"
57
        // frontend requests, which were not triggered by EXT:warming)
NEW
58
        if (!$request->hasHeader(self::REQUEST_HEADER)) {
×
NEW
59
            return null;
×
60
        }
61

NEW
62
        $requestUrl = $this->decryptHeaderValue($request, self::REQUEST_HEADER);
×
63

64
        // Early return if request is invalid (encrypted header value does not match request url)
NEW
65
        if ($requestUrl !== (string)$request->getUri()) {
×
NEW
66
            return null;
×
67
        }
68

NEW
69
        return new UrlMetadata();
×
70
    }
71

72
    /**
73
     * Decrypt and hydrate url metadata object from given response.
74
     */
75
    public function createFromResponse(Message\ResponseInterface $response): ?UrlMetadata
4✔
76
    {
77
        if (!$response->hasHeader(self::RESPONSE_HEADER)) {
4✔
NEW
78
            return null;
×
79
        }
80

81
        try {
82
            $source = new Valinor\Mapper\Source\JsonSource(
4✔
83
                $this->decryptHeaderValue($response, self::RESPONSE_HEADER),
4✔
84
            );
4✔
85

86
            return $this->mapper->map(UrlMetadata::class, $source);
4✔
NEW
87
        } catch (Valinor\Mapper\MappingError|Valinor\Mapper\Source\Exception\InvalidSource) {
×
NEW
88
            return null;
×
89
        }
90
    }
91

92
    /**
93
     * Enrich (prepare) given request for further url metadata enrichment.
94
     */
95
    public function enrichRequest(Message\RequestInterface $request): Message\RequestInterface
4✔
96
    {
97
        return $request->withHeader(
4✔
98
            self::REQUEST_HEADER,
4✔
99
            $this->encryptHeaderValue((string)$request->getUri()),
4✔
100
        );
4✔
101
    }
102

103
    /**
104
     * Enrich given response with decrypted url metadata.
105
     */
NEW
106
    public function enrichResponse(
×
107
        Message\ResponseInterface $response,
108
        UrlMetadata $metadata,
109
    ): Message\ResponseInterface {
NEW
110
        return $response->withHeader(self::RESPONSE_HEADER, $this->encryptHeaderValue($metadata));
×
111
    }
112

113
    /**
114
     * Enrich given response with decrypted url metadata.
115
     */
NEW
116
    public function enrichException(
×
117
        Core\Http\ImmediateResponseException|Core\Error\Http\StatusException $exception,
118
        UrlMetadata $metadata,
119
    ): void {
NEW
120
        if ($exception instanceof Core\Http\ImmediateResponseException) {
×
NEW
121
            $this->injectViaReflection(
×
NEW
122
                $exception,
×
NEW
123
                $this->enrichResponse($exception->getResponse(), $metadata),
×
NEW
124
                'response',
×
NEW
125
            );
×
126
        } else {
NEW
127
            $statusHeaders = $exception->getStatusHeaders();
×
NEW
128
            $statusHeaders[] = sprintf('%s: %s', self::RESPONSE_HEADER, $this->encryptHeaderValue($metadata));
×
129

NEW
130
            $this->injectViaReflection($exception, $statusHeaders, 'statusHeaders');
×
131
        }
132
    }
133

134
    private function encryptHeaderValue(string|\JsonSerializable $value): string
4✔
135
    {
136
        if ($value instanceof \JsonSerializable) {
4✔
NEW
137
            $value = (string)json_encode($value);
×
138
        }
139

140
        if (class_exists(Core\Crypto\HashService::class)) {
4✔
141
            $hashValue = Core\Utility\GeneralUtility::makeInstance(Core\Crypto\HashService::class)->appendHmac(
4✔
142
                $value,
4✔
143
                self::class,
4✔
144
            );
4✔
145
        } else {
146
            // @todo Remove once support for TYPO3 v12 is dropped
147
            /* @phpstan-ignore classConstant.deprecatedClass, method.deprecatedClass */
NEW
148
            $hashValue = Core\Utility\GeneralUtility::makeInstance(Extbase\Security\Cryptography\HashService::class)->appendHmac(
×
NEW
149
                $value,
×
NEW
150
            );
×
151
        }
152

153
        return base64_encode($hashValue);
4✔
154
    }
155

156
    private function decryptHeaderValue(Message\MessageInterface $message, string $headerName): string
4✔
157
    {
158
        $value = base64_decode($message->getHeader($headerName)[0] ?? '', true);
4✔
159

160
        if ($value === false || $value === '') {
4✔
NEW
161
            return '';
×
162
        }
163

164
        if (class_exists(Core\Crypto\HashService::class)) {
4✔
165
            return Core\Utility\GeneralUtility::makeInstance(Core\Crypto\HashService::class)->validateAndStripHmac(
4✔
166
                $value,
4✔
167
                self::class,
4✔
168
            );
4✔
169
        }
170

171
        // @todo Remove once support for TYPO3 v12 is dropped
172
        /* @phpstan-ignore classConstant.deprecatedClass, method.deprecatedClass */
NEW
173
        return Core\Utility\GeneralUtility::makeInstance(Extbase\Security\Cryptography\HashService::class)->validateAndStripHmac(
×
NEW
174
            $value,
×
NEW
175
        );
×
176
    }
177

NEW
178
    private function injectViaReflection(object $object, mixed $value, string $propertyName): void
×
179
    {
NEW
180
        $reflectionObject = new \ReflectionObject($object);
×
NEW
181
        $reflectionProperty = $reflectionObject->getProperty($propertyName);
×
NEW
182
        $reflectionProperty->setValue($object, $value);
×
183
    }
184

185
    /**
186
     * @return Valinor\Mapper\TreeMapper
187
     */
188
    private function createMapper(): Valinor\Mapper\TreeMapper
4✔
189
    {
190
        return (new Valinor\MapperBuilder())
4✔
191
            ->mapper()
4✔
192
        ;
4✔
193
    }
194
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc