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

eliashaeussler / typo3-form-consent / 24130003630

08 Apr 2026 10:10AM UTC coverage: 94.692% (+0.09%) from 94.601%
24130003630

Pull #475

github

eliashaeussler
[TASK] Log unexpected errors during approvals/dismissals
Pull Request #475: [TASK] Log unexpected errors during approvals/dismissals

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

1 existing line in 1 file now uncovered.

785 of 829 relevant lines covered (94.69%)

15.97 hits per line

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

98.92
/Classes/Controller/ConsentController.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\Controller;
25

26
use EliasHaeussler\Typo3FormConsent\Domain;
27
use EliasHaeussler\Typo3FormConsent\Event;
28
use EliasHaeussler\Typo3FormConsent\Registry;
29
use Psr\Http\Message;
30
use Psr\Log;
31
use TYPO3\CMS\Core;
32
use TYPO3\CMS\Extbase;
33

34
/**
35
 * ConsentController
36
 *
37
 * @author Elias Häußler <elias@haeussler.dev>
38
 * @license GPL-2.0-or-later
39
 */
40
final class ConsentController extends Extbase\Mvc\Controller\ActionController
41
{
42
    public function __construct(
20✔
43
        private readonly Domain\Repository\ConsentRepository $consentRepository,
44
        private readonly Extbase\Persistence\PersistenceManagerInterface $persistenceManager,
45
        private readonly Log\LoggerInterface $logger,
46
    ) {}
20✔
47

48
    public function initializeAction(): void
20✔
49
    {
50
        if ($this->isPreviewRequested()) {
20✔
51
            $this->actionMethodName = 'previewAction';
1✔
52
            $this->request = $this->request->withControllerActionName('preview');
1✔
53
            $this->arguments->removeAll();
1✔
54
        }
55
    }
56

57
    /**
58
     * @throws Extbase\Persistence\Exception\IllegalObjectTypeException
59
     * @throws Core\Http\PropagateResponseException
60
     * @throws Extbase\Persistence\Exception\UnknownObjectException
61
     */
62
    public function approveAction(string $hash, string $email, bool $verify = false): Message\ResponseInterface
14✔
63
    {
64
        $consent = $this->consentRepository->findOneByValidationHash($hash);
14✔
65

66
        // Add template variable
67
        $this->view->assign('consent', $consent);
14✔
68

69
        // Early return if consent could not be found
70
        if ($consent === null) {
14✔
71
            return $this->createErrorResponse('invalidConsent');
1✔
72
        }
73

74
        // Early return if given email does not match registered email
75
        if ($email !== $consent->getEmail()) {
13✔
76
            return $this->createErrorResponse('invalidEmail');
1✔
77
        }
78

79
        // Early return if consent is already approved
80
        if ($consent->isApproved()) {
12✔
81
            return $this->createErrorResponse('alreadyApproved');
1✔
82
        }
83

84
        // Render required user verification button
85
        if ($verify) {
12✔
86
            $this->view->assign('verificationNeeded', true);
1✔
87

88
            return $this->createHtmlResponse();
1✔
89
        }
90

91
        // Register consent state
92
        Registry\ConsentManagerRegistry::registerConsent($consent);
12✔
93

94
        // Approve consent
95
        $consent->setApproved();
12✔
96
        $consent->setValidUntil(null);
12✔
97

98
        // Dispatch approve event
99
        try {
100
            $event = new Event\ApproveConsentEvent($consent);
12✔
101
            $this->eventDispatcher->dispatch($event);
12✔
102
        } catch (\Exception $exception) {
1✔
103
            $this->logger->error(
1✔
104
                'Consent approval failed: {message}',
1✔
105
                [
1✔
106
                    'message' => $exception->getMessage(),
1✔
107
                    'hash' => $hash,
1✔
108
                ],
1✔
109
            );
1✔
110

111
            return $this->createErrorResponse('unexpectedError', $exception);
1✔
112
        }
113

114
        // Update approved consent
115
        $this->consentRepository->update($consent);
11✔
116
        $this->persistenceManager->persistAll();
11✔
117

118
        return $this->createHtmlResponse($event->getResponse());
11✔
119
    }
120

121
    /**
122
     * @throws Extbase\Persistence\Exception\IllegalObjectTypeException
123
     * @throws Core\Http\PropagateResponseException
124
     * @throws Extbase\Persistence\Exception\UnknownObjectException
125
     */
126
    public function dismissAction(string $hash, string $email, bool $verify = false): Message\ResponseInterface
9✔
127
    {
128
        $consent = $this->consentRepository->findOneByValidationHash($hash);
9✔
129

130
        // Add template variable
131
        $this->view->assign('consent', $consent);
9✔
132

133
        // Early return if consent could not be found
134
        if ($consent === null) {
9✔
135
            return $this->createErrorResponse('invalidConsent');
1✔
136
        }
137

138
        // Early return if given email does not match registered email
139
        if ($consent->getEmail() !== $email) {
9✔
140
            return $this->createErrorResponse('invalidEmail');
1✔
141
        }
142

143
        // Render required user verification button
144
        if ($verify) {
8✔
145
            $this->view->assign('verificationNeeded', true);
1✔
146

147
            return $this->createHtmlResponse();
1✔
148
        }
149

150
        // Register consent state
151
        Registry\ConsentManagerRegistry::registerConsent($consent);
8✔
152

153
        // Un-approve consent
154
        $consent->setDismissed();
8✔
155
        $consent->setValidUntil(null);
8✔
156

157
        // Dispatch dismiss event
158
        try {
159
            $event = new Event\DismissConsentEvent($consent);
8✔
160
            $this->eventDispatcher->dispatch($event);
8✔
161
        } catch (\Exception $exception) {
1✔
162
            $this->logger->error(
1✔
163
                'Consent dismissal failed: {message}',
1✔
164
                [
1✔
165
                    'message' => $exception->getMessage(),
1✔
166
                    'hash' => $hash,
1✔
167
                ],
1✔
168
            );
1✔
169

170
            return $this->createErrorResponse('unexpectedError', $exception);
1✔
171
        }
172

173
        // Obfuscate submitted data
174
        $consent->setData(null);
7✔
175
        $consent->setOriginalRequestParameters(null);
7✔
176

177
        // Remove dismissed consent
178
        $this->consentRepository->update($consent);
7✔
179
        $this->consentRepository->remove($consent);
7✔
180
        $this->persistenceManager->persistAll();
7✔
181

182
        return $this->createHtmlResponse($event->getResponse());
7✔
183
    }
184

185
    /**
186
     * Dummy preview action for use in backend context.
187
     *
188
     * This action is not part of any frontend plugin. It is used as dummy action for
189
     * preview requests during an active backend session.
190
     *
191
     * NOTE: Method must not be private, otherwise action is not callable by ActionController.
192
     *
193
     * @see ConsentController::initializeAction()
194
     * @see ConsentController::isPreviewRequested()
195
     */
196
    protected function previewAction(): Message\ResponseInterface
1✔
197
    {
198
        return $this->htmlResponse();
1✔
199
    }
200

201
    /**
202
     * @throws Core\Http\PropagateResponseException
203
     */
204
    private function createErrorResponse(string $reason, ?\Throwable $exception = null): Message\ResponseInterface
5✔
205
    {
206
        $this->view->assign('error', true);
5✔
207
        $this->view->assign('reason', $reason);
5✔
208
        $this->view->assign('exception', $exception);
5✔
209

210
        return $this->createHtmlResponse();
5✔
211
    }
212

213
    /**
214
     * @throws Core\Http\PropagateResponseException
215
     */
216
    private function createHtmlResponse(?Message\ResponseInterface $previous = null): Message\ResponseInterface
19✔
217
    {
218
        if ($previous === null) {
19✔
219
            return $this->htmlResponse();
10✔
220
        }
221

222
        if ($previous->getStatusCode() >= 300) {
9✔
223
            throw new Core\Http\PropagateResponseException($previous, 1645646663);
3✔
224
        }
225

226
        $content = (string)$previous->getBody();
6✔
227

228
        if (trim($content) !== '') {
6✔
229
            return $this->htmlResponse($content);
4✔
230
        }
231

232
        return $this->htmlResponse();
2✔
233
    }
234

235
    private function isPreviewRequested(): bool
20✔
236
    {
237
        // Early return if no backend session is active
238
        if ($this->getBackendUser() === null) {
20✔
239
            return false;
20✔
240
        }
241

242
        // Early return if at least one argument is given
243
        if ($this->request->getArguments() !== []) {
1✔
UNCOV
244
            return false;
×
245
        }
246

247
        return true;
1✔
248
    }
249

250
    private function getBackendUser(): ?Core\Authentication\BackendUserAuthentication
20✔
251
    {
252
        $backendUser = $GLOBALS['BE_USER'] ?? null;
20✔
253

254
        if ($backendUser instanceof Core\Authentication\BackendUserAuthentication) {
20✔
255
            return $backendUser;
1✔
256
        }
257

258
        return null;
20✔
259
    }
260
}
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