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

TYPO3-Headless / headless / 26439917307

26 May 2026 07:58AM UTC coverage: 70.455% (-5.0%) from 75.459%
26439917307

Pull #893

github

web-flow
Merge 00997f766 into 79b7c5472
Pull Request #893: [TASK] Reintroduce missing features, extension cleanup

360 of 523 new or added lines in 32 files covered. (68.83%)

167 existing lines in 7 files now uncovered.

1364 of 1936 relevant lines covered (70.45%)

7.36 hits per line

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

0.0
/Classes/ViewHelpers/LoginFormViewHelper.php
1
<?php
2

3
/*
4
 * This file is part of the "headless" Extension for TYPO3 CMS.
5
 *
6
 * For the full copyright and license information, please read the
7
 * LICENSE.md file that was distributed with this source code.
8
 */
9

10
declare(strict_types=1);
11

12
namespace FriendsOfTYPO3\Headless\ViewHelpers;
13

14
use LogicException;
15
use RuntimeException;
16
use TYPO3\CMS\Core\Context\Context;
17
use TYPO3\CMS\Core\Context\SecurityAspect;
18
use TYPO3\CMS\Core\Crypto\HashAlgo;
19
use TYPO3\CMS\Core\Security\RequestToken;
20
use TYPO3\CMS\Core\Utility\GeneralUtility;
21
use TYPO3\CMS\Extbase\DomainObject\AbstractDomainObject;
22
use TYPO3\CMS\Extbase\Mvc\RequestInterface;
23
use TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy;
24
use TYPO3\CMS\Extbase\Security\HashScope;
25
use TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper;
26

27
use function base64_encode;
28
use function is_int;
29
use function is_object;
30
use function is_string;
31
use function json_encode;
32

33
use function serialize;
34
use function sprintf;
35
use function strtolower;
36

37
use const JSON_THROW_ON_ERROR;
38

39
/**
40
 * Form ViewHelper. Generates a :html:`<form>` Tag.
41
 *
42
 * Basic usage
43
 * ===========
44
 *
45
 * Use :html:`<f:form>` to output an HTML :html:`<form>` tag which is targeted
46
 * at the specified action, in the current controller and package.
47
 * It will submit the form data via a POST request. If you want to change this,
48
 * use :html:`method="get"` as an argument.
49
 *
50
 * Examples
51
 * ========
52
 *
53
 * A complex form with a specified encoding type
54
 * ---------------------------------------------
55
 *
56
 * Form with enctype set::
57
 *
58
 *    <f:form action=".." controller="..." package="..." enctype="multipart/form-data">...</f:form>
59
 *
60
 * A Form which should render a domain object
61
 * ------------------------------------------
62
 *
63
 * Binding a domain object to a form::
64
 *
65
 *    <f:form action="..." name="customer" object="{customer}">
66
 *       <f:form.hidden property="id" />
67
 *       <f:form.textbox property="name" />
68
 *    </f:form>
69
 *
70
 * This automatically inserts the value of ``{customer.name}`` inside the
71
 * textbox and adjusts the name of the textbox accordingly.
72
 */
73
class LoginFormViewHelper extends FormViewHelper
74
{
75
    /**
76
     * @var array<int, array<string, mixed>>
77
     */
78
    protected array $data = [];
79
    protected int $i = 0;
80

81
    /**
82
     * Render the form.
83
     *
84
     * @return string rendered form
85
     */
86
    public function render(): string
87
    {
UNCOV
88
        $renderingContext = $this->renderingContext;
×
UNCOV
89
        $request = $renderingContext->getRequest();
×
UNCOV
90
        if (!$request instanceof RequestInterface) {
×
UNCOV
91
            throw new RuntimeException(
×
UNCOV
92
                'ViewHelper f:form can be used only in extbase context and needs a request implementing extbase RequestInterface.',
×
UNCOV
93
                1639821904
×
UNCOV
94
            );
×
95
        }
96

UNCOV
97
        $this->setFormActionUri();
×
98

99
        // Force 'method="get"' or 'method="post"', defaulting to "post".
UNCOV
100
        if (isset($this->arguments['method']) && strtolower($this->arguments['method']) === 'get') {
×
UNCOV
101
            $this->tag->addAttribute('method', 'get');
×
102
        } else {
UNCOV
103
            $this->tag->addAttribute('method', 'post');
×
104
        }
105

UNCOV
106
        if (isset($this->arguments['novalidate']) && $this->arguments['novalidate'] === true) {
×
UNCOV
107
            $this->tag->addAttribute('novalidate', 'novalidate');
×
108
        }
109

UNCOV
110
        $this->addFormObjectNameToViewHelperVariableContainer();
×
UNCOV
111
        $this->addFormObjectToViewHelperVariableContainer();
×
UNCOV
112
        $this->addFieldNamePrefixToViewHelperVariableContainer();
×
UNCOV
113
        $this->addFormFieldNamesToViewHelperVariableContainer();
×
114

UNCOV
115
        $this->data = $this->renderChildren();
×
116

UNCOV
117
        $this->renderHiddenIdentityField($this->arguments['object'] ?? null, $this->getFormObjectName());
×
UNCOV
118
        $this->renderAdditionalIdentityFields();
×
UNCOV
119
        $this->renderHiddenReferrerFields();
×
UNCOV
120
        $this->renderRequestTokenHiddenField();
×
121

122
        // Render the trusted list of all properties after everything else has been rendered
UNCOV
123
        $this->renderTrustedPropertiesField();
×
124

UNCOV
125
        $this->removeFieldNamePrefixFromViewHelperVariableContainer();
×
UNCOV
126
        $this->removeFormObjectFromViewHelperVariableContainer();
×
UNCOV
127
        $this->removeFormObjectNameFromViewHelperVariableContainer();
×
UNCOV
128
        $this->removeFormFieldNamesFromViewHelperVariableContainer();
×
UNCOV
129
        $this->removeCheckboxFieldNamesFromViewHelperVariableContainer();
×
130

NEW
131
        return json_encode($this->data, JSON_THROW_ON_ERROR);
×
132
    }
133

134
    /**
135
     * Render additional identity fields which were registered by form elements.
136
     * This happens if a form field is defined like property="bla.blubb" - then we might need an identity property for the sub-object "bla".
137
     *
138
     * @return string HTML-string for the additional identity properties
139
     */
140
    protected function renderAdditionalIdentityFields(): string
141
    {
UNCOV
142
        if ($this->viewHelperVariableContainer->exists(FormViewHelper::class, 'additionalIdentityProperties')) {
×
UNCOV
143
            $additionalIdentityProperties = $this->viewHelperVariableContainer->get(FormViewHelper::class, 'additionalIdentityProperties');
×
UNCOV
144
            foreach ($additionalIdentityProperties as $identity) {
×
UNCOV
145
                $this->addHiddenField('identity', $identity);
×
146
            }
147
        }
148

UNCOV
149
        return '';
×
150
    }
151

152
    /**
153
     * Renders hidden form fields for referrer information about
154
     * the current controller and action.
155
     *
156
     * @return string Hidden fields with referrer information
157
     * @todo filter out referrer information that is equal to the target (e.g. same packageKey)
158
     */
159
    protected function renderHiddenReferrerFields(): string
160
    {
UNCOV
161
        $renderingContext = $this->renderingContext;
×
162
        /** @var RequestInterface $request */
UNCOV
163
        $request = $renderingContext->getRequest();
×
UNCOV
164
        $extensionName = $request->getControllerExtensionName();
×
UNCOV
165
        $controllerName = $request->getControllerName();
×
UNCOV
166
        $actionName = $request->getControllerActionName();
×
UNCOV
167
        $actionRequest = [
×
UNCOV
168
            '@extension' => $extensionName,
×
UNCOV
169
            '@controller' => $controllerName,
×
UNCOV
170
            '@action' => $actionName,
×
UNCOV
171
        ];
×
172

UNCOV
173
        $this->addHiddenField(
×
NEW
174
            $this->prefixFieldName('__referrer[@extension]'),
×
UNCOV
175
            $extensionName
×
UNCOV
176
        );
×
UNCOV
177
        $this->addHiddenField(
×
NEW
178
            $this->prefixFieldName('__referrer[@controller]'),
×
UNCOV
179
            $controllerName
×
UNCOV
180
        );
×
UNCOV
181
        $this->addHiddenField(
×
NEW
182
            $this->prefixFieldName('__referrer[@action]'),
×
UNCOV
183
            $actionName
×
UNCOV
184
        );
×
UNCOV
185
        $this->addHiddenField(
×
NEW
186
            $this->prefixFieldName('__referrer[arguments]'),
×
UNCOV
187
            $this->hashService->appendHmac(
×
UNCOV
188
                base64_encode(serialize($request->getArguments())),
×
NEW
189
                HashScope::ReferringArguments->prefix(),
×
NEW
190
                HashAlgo::SHA3_256
×
UNCOV
191
            )
×
UNCOV
192
        );
×
UNCOV
193
        $this->addHiddenField(
×
NEW
194
            $this->prefixFieldName('__referrer[@request]'),
×
UNCOV
195
            $this->hashService->appendHmac(
×
NEW
196
                json_encode($actionRequest, JSON_THROW_ON_ERROR),
×
NEW
197
                HashScope::ReferringRequest->prefix(),
×
NEW
198
                HashAlgo::SHA3_256
×
UNCOV
199
            )
×
UNCOV
200
        );
×
201

UNCOV
202
        return '';
×
203
    }
204

205
    /**
206
     * Adds the field name prefix to the ViewHelperVariableContainer
207
     */
208
    protected function addFieldNamePrefixToViewHelperVariableContainer(): void
209
    {
UNCOV
210
        $fieldNamePrefix = $this->getFieldNamePrefix();
×
UNCOV
211
        $this->viewHelperVariableContainer->add(FormViewHelper::class, 'fieldNamePrefix', $fieldNamePrefix);
×
212
    }
213

214
    /**
215
     * Renders a hidden form field containing the technical identity of the given object.
216
     *
217
     * @param mixed $object Object to create the identity field for. Non-objects are ignored.
218
     * @param string|null $name Name
219
     * @return string A hidden field containing the Identity (uid) of the given object
220
     * @see \TYPO3\CMS\Extbase\Mvc\Controller\Argument::setValue()
221
     */
222
    protected function renderHiddenIdentityField(mixed $object, ?string $name): string
223
    {
UNCOV
224
        if ($object instanceof LazyLoadingProxy) {
×
UNCOV
225
            $object = $object->_loadRealInstance();
×
226
        }
UNCOV
227
        if (!is_object($object)
×
UNCOV
228
            || !($object instanceof AbstractDomainObject)
×
UNCOV
229
            || ($object->_isNew() && !$object->_isClone())
×
230
        ) {
UNCOV
231
            return '';
×
232
        }
233
        // Intentionally NOT using PersistenceManager::getIdentifierByObject here.
234
        // Using that one breaks re-submission of data in forms in case of an error.
UNCOV
235
        $identifier = $object->getUid();
×
UNCOV
236
        if ($identifier === null) {
×
UNCOV
237
            return '';
×
238
        }
UNCOV
239
        $name = $this->prefixFieldName($name ?? '') . '[__identity]';
×
UNCOV
240
        $this->registerFieldNameForFormTokenGeneration($name);
×
241

UNCOV
242
        $this->addHiddenField($name, $identifier);
×
243

NEW
244
        return '';
×
245
    }
246

247
    /**
248
     * Render the request hash field
249
     */
250
    protected function renderTrustedPropertiesField(): string
251
    {
UNCOV
252
        $formFieldNames
×
UNCOV
253
            = $this->viewHelperVariableContainer->get(
×
UNCOV
254
                FormViewHelper::class,
×
UNCOV
255
                'formFieldNames'
×
UNCOV
256
            );
×
UNCOV
257
        $requestHash
×
UNCOV
258
            = $this->mvcPropertyMappingConfigurationService->generateTrustedPropertiesToken(
×
UNCOV
259
                $formFieldNames,
×
UNCOV
260
                $this->getFieldNamePrefix()
×
UNCOV
261
            );
×
UNCOV
262
        $this->addHiddenField('__trustedProperties', $requestHash);
×
263

UNCOV
264
        return '';
×
265
    }
266

267
    protected function renderRequestTokenHiddenField(): string
268
    {
UNCOV
269
        $requestToken = $this->arguments['requestToken'] ?? null;
×
UNCOV
270
        $signingType = $this->arguments['signingType'] ?? null;
×
271

UNCOV
272
        $isTrulyRequestToken = is_int($requestToken) && $requestToken === 1
×
UNCOV
273
            || is_string($requestToken) && strtolower($requestToken) === 'true';
×
UNCOV
274
        $formAction = $this->tag->getAttribute('action');
×
275

276
        // basically "request token, yes" - uses form-action URI as scope
UNCOV
277
        if ($isTrulyRequestToken || $requestToken === '@nonce') {
×
UNCOV
278
            $requestToken = RequestToken::create($formAction);
×
279
            // basically "request token with 'my-scope'" - uses 'my-scope'
UNCOV
280
        } elseif (is_string($requestToken) && $requestToken !== '') {
×
UNCOV
281
            $requestToken = RequestToken::create($requestToken);
×
282
        }
UNCOV
283
        if (!$requestToken instanceof RequestToken) {
×
UNCOV
284
            return '';
×
285
        }
UNCOV
286
        if (strtolower((string)($this->arguments['method'] ?? '')) === 'get') {
×
UNCOV
287
            throw new LogicException('Cannot apply request token for forms sent via HTTP GET', 1651775963);
×
288
        }
289

UNCOV
290
        $context = GeneralUtility::makeInstance(Context::class);
×
UNCOV
291
        $securityAspect = SecurityAspect::provideIn($context);
×
292
        // @todo currently defaults to 'nonce', there might be a better strategy in the future
UNCOV
293
        $signingType = $signingType ?: 'nonce';
×
UNCOV
294
        $signingProvider = $securityAspect->getSigningSecretResolver()->findByType($signingType);
×
UNCOV
295
        if ($signingProvider === null) {
×
UNCOV
296
            throw new LogicException(sprintf('Cannot find request token signing type "%s"', $signingType), 1664260307);
×
297
        }
298

UNCOV
299
        $signingSecret = $signingProvider->provideSigningSecret();
×
UNCOV
300
        $requestToken = $requestToken->withMergedParams(['request' => ['uri' => $formAction]]);
×
301

UNCOV
302
        $this->addHiddenField(RequestToken::PARAM_NAME, $requestToken->toHashSignedJwt($signingSecret));
×
303

UNCOV
304
        return '';
×
305
    }
306

307
    protected function addHiddenField(string $name, mixed $value): void
308
    {
UNCOV
309
        $tmp = [];
×
UNCOV
310
        $tmp['name'] = $name;
×
UNCOV
311
        $tmp['type'] = 'hidden';
×
UNCOV
312
        $tmp['value'] = $value;
×
UNCOV
313
        $this->data[] = $tmp;
×
314
    }
315
}
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