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

eliashaeussler / typo3-form-consent / 26598416981

28 May 2026 07:50PM UTC coverage: 91.598% (-3.1%) from 94.724%
26598416981

Pull #495

github

eliashaeussler
[TASK] Restructure CI and build tools
Pull Request #495: [TASK] Restructure CI and build tools

1 of 1 new or added line in 1 file covered. (100.0%)

36 existing lines in 3 files now uncovered.

774 of 845 relevant lines covered (91.6%)

16.06 hits per line

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

80.85
/Classes/Event/Listener/InvokeFinishersListener.php
1
<?php
2

3
declare(strict_types=1);
4

5
/*
6
 * This file is part of the TYPO3 CMS extension "form_consent".
7
 *
8
 * Copyright (C) 2021-2026 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\Typo3FormConsent\Event\Listener;
25

26
use Derhansen\FormCrshield;
27
use EliasHaeussler\Typo3FormConsent\Compatibility;
28
use EliasHaeussler\Typo3FormConsent\Domain;
29
use EliasHaeussler\Typo3FormConsent\Event;
30
use EliasHaeussler\Typo3FormConsent\Type;
31
use Psr\Http\Message;
32
use TYPO3\CMS\Core;
33
use TYPO3\CMS\Extbase;
34
use TYPO3\CMS\Form;
35
use TYPO3\CMS\Frontend;
36

37
/**
38
 * InvokeFinishersListener
39
 *
40
 * @author Elias Häußler <elias@haeussler.dev>
41
 * @license GPL-2.0-or-later
42
 * @internal
43
 */
44
final readonly class InvokeFinishersListener
45
{
46
    private Core\Information\Typo3Version $typo3Version;
47

48
    public function __construct(
18✔
49
        private Extbase\Configuration\ConfigurationManagerInterface $extbaseConfigurationManager,
50
        private Form\Mvc\Configuration\ConfigurationManagerInterface $formConfigurationManager,
51
        private Form\Mvc\Persistence\FormPersistenceManagerInterface $formPersistenceManager,
52
        private Compatibility\Migration\HmacHashMigration $hmacHashMigration,
53
        private Core\Domain\Repository\PageRepository $pageRepository,
54
    ) {
55
        $this->typo3Version = new Core\Information\Typo3Version();
18✔
56
    }
57

58
    #[Core\Attribute\AsEventListener('formConsentInvokeFinishersOnConsentApproveListener')]
12✔
59
    public function onConsentApprove(Event\ApproveConsentEvent $event): void
60
    {
61
        $response = $this->invokeFinishers($event->getConsent(), 'isConsentApproved()');
12✔
62
        $event->setResponse($response);
12✔
63
    }
64

65
    #[Core\Attribute\AsEventListener('formConsentInvokeFinishersOnConsentDismissListener')]
8✔
66
    public function onConsentDismiss(Event\DismissConsentEvent $event): void
67
    {
68
        $response = $this->invokeFinishers($event->getConsent(), 'isConsentDismissed()');
8✔
69
        $event->setResponse($response);
8✔
70
    }
71

72
    private function invokeFinishers(Domain\Model\Consent $consent, string $condition): ?Message\ResponseInterface
18✔
73
    {
74
        // Early return if original request is missing
75
        // or no finisher variants are configured
76
        if (
77
            $consent->getOriginalRequestParameters() === null
18✔
78
            || $consent->getOriginalContentElementUid() === 0
18✔
79
            || !$this->areFinisherVariantsConfigured($consent->getFormPersistenceIdentifier(), $condition)
18✔
80
        ) {
81
            return null;
7✔
82
        }
83

84
        // Migrate legacy HMAC hashes after upgrade to TYPO3 v13
85
        $consent->setOriginalRequestParameters(
11✔
86
            $this->migrateOriginalRequestParameters($consent->getOriginalRequestParameters()),
11✔
87
        );
11✔
88

89
        // Re-render form to invoke finishers
90
        $request = $this->createRequestFromOriginalRequestParameters($consent->getOriginalRequestParameters());
11✔
91

92
        return $this->dispatchFormReRendering($consent, $request);
11✔
93
    }
94

95
    private function dispatchFormReRendering(
11✔
96
        Domain\Model\Consent $consent,
97
        Message\ServerRequestInterface $serverRequest,
98
    ): ?Message\ResponseInterface {
99
        // Fetch record of original content element
100
        $contentElementRecord = $this->fetchOriginalContentElementRecord($consent->getOriginalContentElementUid());
11✔
101

102
        // Early return if content element record cannot be resolved
103
        if (!\is_array($contentElementRecord)) {
11✔
104
            return null;
×
105
        }
106

107
        // Build extbase bootstrap object
108
        $contentObjectRenderer = Core\Utility\GeneralUtility::makeInstance(Frontend\ContentObject\ContentObjectRenderer::class);
11✔
109
        $contentObjectRenderer->setRequest($serverRequest);
11✔
110
        $contentObjectRenderer->start($contentElementRecord, 'tt_content');
11✔
111
        $contentObjectRenderer->setUserObjectType(Frontend\ContentObject\ContentObjectRenderer::OBJECTTYPE_USER_INT);
11✔
112
        $bootstrap = Core\Utility\GeneralUtility::makeInstance(Extbase\Core\Bootstrap::class);
11✔
113
        $bootstrap->setContentObjectRenderer($contentObjectRenderer);
11✔
114

115
        // Inject content object renderer
116
        $serverRequest = $serverRequest->withAttribute('currentContentObject', $contentObjectRenderer);
11✔
117

118
        $configuration = [
11✔
119
            'extensionName' => 'Form',
11✔
120
            'pluginName' => 'Formframework',
11✔
121
        ];
11✔
122

123
        // Prepare clean environment
124
        $globalsBackup = $GLOBALS;
11✔
125
        $this->disableThirdPartyHooks();
11✔
126

127
        try {
128
            // Dispatch extbase request
129
            $content = $bootstrap->run('', $configuration, $serverRequest);
11✔
130
            $response = new Core\Http\Response();
8✔
131
            $response->getBody()->write($content);
8✔
132

133
            return $response;
8✔
134
        } catch (Core\Http\ImmediateResponseException|Core\Http\PropagateResponseException $exception) {
3✔
135
            // If any immediate response is thrown, use this for further processing
136
            return $exception->getResponse();
3✔
137
        } finally {
138
            // Restore previous environment
139
            foreach ($globalsBackup as $key => $value) {
11✔
140
                $GLOBALS[$key] = $value;
11✔
141
            }
142
        }
143
    }
144

145
    /**
146
     * @param Type\JsonType<string, array<string, array<string, mixed>>> $originalRequestParameters
147
     * @return Type\JsonType<string, array<string, array<string, mixed>>>
148
     */
149
    private function migrateOriginalRequestParameters(Type\JsonType $originalRequestParameters): Type\JsonType
11✔
150
    {
151
        $parameters = $originalRequestParameters->toArray();
11✔
152

153
        array_walk_recursive($parameters, function (mixed &$value, string|int $key): void {
11✔
154
            if (!is_string($value) || !is_string($key)) {
11✔
155
                return;
×
156
            }
157

158
            // Migrate EXT:extbase and EXT:form hash scopes
159
            $hashScope = Form\Security\HashScope::tryFrom($key);
11✔
160
            $hashScope ??= Extbase\Security\HashScope::tryFrom($key);
11✔
161

162
            if ($hashScope !== null) {
11✔
163
                $value = $this->hmacHashMigration->migrate($value, $hashScope);
11✔
164
            }
165
        });
11✔
166

167
        return Type\JsonType::fromArray($parameters);
11✔
168
    }
169

170
    /**
171
     * @return array<string, mixed>|null
172
     */
173
    private function fetchOriginalContentElementRecord(int $contentElementUid): ?array
11✔
174
    {
175
        // Early return if content element UID cannot be  determined
176
        if ($contentElementUid === 0) {
11✔
177
            return null;
×
178
        }
179

180
        // Fetch content element record
181
        $record = $this->pageRepository->checkRecord('tt_content', $contentElementUid);
11✔
182

183
        // Early return if content element record cannot be resolved
184
        if (!\is_array($record)) {
11✔
185
            return null;
×
186
        }
187

188
        return $this->pageRepository->getLanguageOverlay('tt_content', $record);
11✔
189
    }
190

191
    /**
192
     * @param Type\JsonType<string, array<string, array<string, mixed>>> $originalRequestParameters
193
     */
194
    private function createRequestFromOriginalRequestParameters(Type\JsonType $originalRequestParameters): Message\ServerRequestInterface
11✔
195
    {
196
        return $this->getServerRequest()
11✔
197
            ->withMethod('POST')
11✔
198
            ->withParsedBody($originalRequestParameters->toArray());
11✔
199
    }
200

201
    /**
202
     * @todo Remove once support for EXT:form_crshield v3 / TYPO3 v13 is dropped
203
     */
204
    private function disableThirdPartyHooks(): void
11✔
205
    {
206
        // Hooks from EXT:form_crshield must be disabled since they would avoid successful re-rendering
207
        if (class_exists(FormCrshield\Hooks\Form::class)) {
11✔
UNCOV
208
            $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterInitializeCurrentPage'] = array_diff(
×
UNCOV
209
                $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterInitializeCurrentPage'],
×
UNCOV
210
                [FormCrshield\Hooks\Form::class],
×
UNCOV
211
            );
×
UNCOV
212
            $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterSubmit'] = array_diff(
×
UNCOV
213
                $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterSubmit'],
×
UNCOV
214
                [FormCrshield\Hooks\Form::class],
×
UNCOV
215
            );
×
216
        }
217
    }
218

219
    private function areFinisherVariantsConfigured(string $formPersistenceIdentifier, string $condition): bool
18✔
220
    {
221
        $typoScriptSettings = $this->extbaseConfigurationManager->getConfiguration(
18✔
222
            Extbase\Configuration\ConfigurationManagerInterface::CONFIGURATION_TYPE_SETTINGS,
18✔
223
            'form',
18✔
224
        );
18✔
225

226
        if ($this->typo3Version->getMajorVersion() >= 14) {
18✔
227
            $formConfiguration = $this->formPersistenceManager->load($formPersistenceIdentifier, $typoScriptSettings);
18✔
228
        } else {
229
            // @todo Remove once support for TYPO3 v13 is dropped
UNCOV
230
            $formSettings = $this->formConfigurationManager->getYamlConfiguration($typoScriptSettings, true);
×
UNCOV
231
            $formConfiguration = $this->formPersistenceManager->load(
×
UNCOV
232
                $formPersistenceIdentifier,
×
UNCOV
233
                $formSettings,
×
UNCOV
234
                $typoScriptSettings,
×
UNCOV
235
            );
×
236
        }
237

238
        foreach ($formConfiguration['variants'] ?? [] as $variant) {
18✔
239
            if (str_contains($variant['condition'] ?? '', $condition) && isset($variant['finishers'])) {
11✔
240
                return true;
11✔
241
            }
242
        }
243

244
        return false;
7✔
245
    }
246

247
    private function getServerRequest(): Message\ServerRequestInterface
11✔
248
    {
249
        return $GLOBALS['TYPO3_REQUEST'] ?? Core\Http\ServerRequestFactory::fromGlobals();
11✔
250
    }
251
}
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

© 2026 Coveralls, Inc