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

TYPO3-Headless / headless / 14022693367

23 Mar 2025 08:26PM UTC coverage: 52.345% (-20.8%) from 73.13%
14022693367

Pull #815

github

web-flow
Merge e0fcdaa4a into a15e1c8c4
Pull Request #815: [FEATURE] Add support for f:form.* viewhelper

0 of 600 new or added lines in 15 files covered. (0.0%)

1105 of 2111 relevant lines covered (52.34%)

5.94 hits per line

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

0.0
/Classes/XClass/ViewHelpers/FormViewHelper.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
namespace FriendsOfTYPO3\Headless\XClass\ViewHelpers;
11

12
use LogicException;
13
use Psr\Http\Message\ServerRequestInterface;
14
use RuntimeException;
15
use TYPO3\CMS\Core\Context\Context;
16
use TYPO3\CMS\Core\Context\SecurityAspect;
17
use TYPO3\CMS\Core\Security\RequestToken;
18
use TYPO3\CMS\Core\Utility\GeneralUtility;
19
use TYPO3\CMS\Extbase\DomainObject\AbstractDomainObject;
20
use TYPO3\CMS\Extbase\Mvc\RequestInterface;
21
use TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy;
22
use TYPO3\CMS\Extbase\Security\HashScope;
23

24
class FormViewHelper extends \TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper
25
{
26
    protected array $data = [];
27

28
    public function render(): string
29
    {
NEW
30
        foreach ($this->tag->getAttributes() as $key => $value) {
×
NEW
31
            if (str_starts_with($key, 'data-')) {
×
NEW
32
                $key = substr($key, strpos('data-', $key) + 5);
×
NEW
33
                $this->data['data'][$key] = $value;
×
NEW
34
                continue;
×
35
            }
36

NEW
37
            $this->data[$key] = $value;
×
38
        }
39

NEW
40
        if (!$this->renderingContext->hasAttribute(ServerRequestInterface::class)
×
NEW
41
            || !$this->renderingContext->getAttribute(ServerRequestInterface::class) instanceof RequestInterface) {
×
NEW
42
            throw new RuntimeException(
×
NEW
43
                'ViewHelper f:form can be used only in extbase context and needs a request implementing extbase RequestInterface.',
×
NEW
44
                1639821904
×
NEW
45
            );
×
46
        }
47

NEW
48
        $this->setFormActionUri();
×
49

NEW
50
        if ($this->tag->getAttribute('action') !== null) {
×
NEW
51
            $this->data['action'] = $this->tag->getAttribute('action');
×
52
        }
53

54
        // Force 'method="get"' or 'method="post"', defaulting to "post".
NEW
55
        if (isset($this->arguments['method']) && strtolower($this->arguments['method']) === 'get') {
×
NEW
56
            $this->data['method'] = 'get';
×
57
        } else {
NEW
58
            $this->data['method'] = 'post';
×
59
        }
60

NEW
61
        if (!empty($this->arguments['name'])) {
×
NEW
62
            $this->data['name'] = $this->arguments['name'];
×
63
        }
64

NEW
65
        if (isset($this->arguments['novalidate']) && $this->arguments['novalidate'] === true) {
×
NEW
66
            $this->data['novalidate'] = 'novalidate';
×
67
        }
68

NEW
69
        $this->addFormObjectNameToViewHelperVariableContainer();
×
NEW
70
        $this->addFormObjectToViewHelperVariableContainer();
×
NEW
71
        $this->addFieldNamePrefixToViewHelperVariableContainer();
×
NEW
72
        $this->addFormFieldNamesToViewHelperVariableContainer();
×
73

NEW
74
        $children = trim($this->renderChildren());
×
NEW
75
        $children = preg_replace('!}\s*{!', '},{', $children);
×
NEW
76
        $children = preg_replace("!\r?\n!", '', $children);
×
NEW
77
        $children = '{"elements": [' . $children . ']}';
×
NEW
78
        $children = json_decode($children, true);
×
79

NEW
80
        if ($children !== null && isset($children['elements'])) {
×
NEW
81
            $this->data['elements'] = $children['elements'];
×
82
        }
83

NEW
84
        if (isset($this->arguments['hiddenFieldClassName']) && $this->arguments['hiddenFieldClassName'] !== null) {
×
NEW
85
            $this->data['hiddenFieldClassName'] = htmlspecialchars($this->arguments['hiddenFieldClassName']);
×
86
        }
87

NEW
88
        $this->renderHiddenIdentityField($this->arguments['object'] ?? null, $this->getFormObjectName());
×
NEW
89
        $this->renderAdditionalIdentityFields();
×
NEW
90
        $this->renderHiddenReferrerFields();
×
NEW
91
        $this->renderRequestTokenHiddenField();
×
92

93
        // Render the trusted list of all properties after everything else has been rendered
NEW
94
        $this->renderTrustedPropertiesField();
×
95

96
        //$content .= $formContent;
NEW
97
        $this->removeFieldNamePrefixFromViewHelperVariableContainer();
×
NEW
98
        $this->removeFormObjectFromViewHelperVariableContainer();
×
NEW
99
        $this->removeFormObjectNameFromViewHelperVariableContainer();
×
NEW
100
        $this->removeFormFieldNamesFromViewHelperVariableContainer();
×
NEW
101
        $this->removeCheckboxFieldNamesFromViewHelperVariableContainer();
×
102

NEW
103
        return json_encode($this->data);
×
104
    }
105

106
    /**
107
     * Render the request hash field
108
     */
109
    protected function renderTrustedPropertiesField(): string
110
    {
NEW
111
        $formFieldNames = $this->renderingContext->getViewHelperVariableContainer()->get(\TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class, 'formFieldNames');
×
NEW
112
        $requestHash = $this->mvcPropertyMappingConfigurationService->generateTrustedPropertiesToken($formFieldNames, $this->getFieldNamePrefix());
×
113

NEW
114
        $this->addHiddenField('__trustedProperties', $requestHash);
×
115

NEW
116
        return '';
×
117
    }
118

119
    /**
120
     * Renders a hidden form field containing the technical identity of the given object.
121
     *
122
     * @param mixed $object Object to create the identity field for. Non-objects are ignored.
123
     * @param string|null $name Name
124
     * @return string A hidden field containing the Identity (uid) of the given object
125
     * @see \TYPO3\CMS\Extbase\Mvc\Controller\Argument::setValue()
126
     */
127
    protected function renderHiddenIdentityField(mixed $object, ?string $name): string
128
    {
NEW
129
        if ($object instanceof LazyLoadingProxy) {
×
NEW
130
            $object = $object->_loadRealInstance();
×
131
        }
NEW
132
        if (!is_object($object)
×
NEW
133
            || !($object instanceof AbstractDomainObject)
×
NEW
134
            || ($object->_isNew() && !$object->_isClone())) {
×
NEW
135
            return '';
×
136
        }
137
        // Intentionally NOT using PersistenceManager::getIdentifierByObject here.
138
        // Using that one breaks re-submission of data in forms in case of an error.
NEW
139
        $identifier = $object->getUid();
×
NEW
140
        if ($identifier === null) {
×
NEW
141
            return '';
×
142
        }
NEW
143
        $name = $this->prefixFieldName($name ?? '') . '[__identity]';
×
NEW
144
        $this->registerFieldNameForFormTokenGeneration($name);
×
145

NEW
146
        $this->addHiddenField($name, $identifier);
×
147

NEW
148
        return '';
×
149
    }
150

151
    protected function renderRequestTokenHiddenField(): string
152
    {
NEW
153
        $requestToken = $this->arguments['requestToken'] ?? null;
×
NEW
154
        $signingType = $this->arguments['signingType'] ?? null;
×
155

NEW
156
        $isTrulyRequestToken = is_int($requestToken) && $requestToken === 1
×
NEW
157
            || is_string($requestToken) && strtolower($requestToken) === 'true';
×
NEW
158
        $formAction = $this->tag->getAttribute('action');
×
159

160
        // basically "request token, yes" - uses form-action URI as scope
NEW
161
        if ($isTrulyRequestToken || $requestToken === '@nonce') {
×
NEW
162
            $requestToken = RequestToken::create($formAction);
×
163
            // basically "request token with 'my-scope'" - uses 'my-scope'
NEW
164
        } elseif (is_string($requestToken) && $requestToken !== '') {
×
NEW
165
            $requestToken = RequestToken::create($requestToken);
×
166
        }
NEW
167
        if (!$requestToken instanceof RequestToken) {
×
NEW
168
            return '';
×
169
        }
NEW
170
        if (strtolower((string)($this->arguments['method'] ?? '')) === 'get') {
×
NEW
171
            throw new LogicException('Cannot apply request token for forms sent via HTTP GET', 1651775963);
×
172
        }
173

NEW
174
        $context = GeneralUtility::makeInstance(Context::class);
×
NEW
175
        $securityAspect = SecurityAspect::provideIn($context);
×
176
        // @todo currently defaults to 'nonce', there might be a better strategy in the future
NEW
177
        $signingType = $signingType ?: 'nonce';
×
NEW
178
        $signingProvider = $securityAspect->getSigningSecretResolver()->findByType($signingType);
×
NEW
179
        if ($signingProvider === null) {
×
NEW
180
            throw new LogicException(sprintf('Cannot find request token signing type "%s"', $signingType), 1664260307);
×
181
        }
182

NEW
183
        $signingSecret = $signingProvider->provideSigningSecret();
×
NEW
184
        $requestToken = $requestToken->withMergedParams(['request' => ['uri' => $formAction]]);
×
185

NEW
186
        $this->addHiddenField(RequestToken::PARAM_NAME, $requestToken->toHashSignedJwt($signingSecret));
×
187

NEW
188
        return '';
×
189
    }
190

191
    /**
192
     * Render additional identity fields which were registered by form elements.
193
     * This happens if a form field is defined like property="bla.blubb" - then we might need an identity property for the sub-object "bla".
194
     *
195
     * @return string HTML-string for the additional identity properties
196
     */
197
    protected function renderAdditionalIdentityFields(): string
198
    {
NEW
199
        if ($this->viewHelperVariableContainer->exists(\TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class, 'additionalIdentityProperties')) {
×
NEW
200
            $additionalIdentityProperties = $this->viewHelperVariableContainer->get(FormViewHelper::class, 'additionalIdentityProperties');
×
NEW
201
            foreach ($additionalIdentityProperties as $identity) {
×
NEW
202
                $this->addHiddenField('identity', $identity);
×
203
            }
204
        }
205

NEW
206
        return '';
×
207
    }
208

209
    protected function renderHiddenReferrerFields(): string
210
    {
211
        /** @var RequestInterface $request */
NEW
212
        $request = $this->renderingContext->getAttribute(ServerRequestInterface::class);
×
NEW
213
        $extensionName = $request->getControllerExtensionName();
×
NEW
214
        $controllerName = $request->getControllerName();
×
NEW
215
        $actionName = $request->getControllerActionName();
×
NEW
216
        $actionRequest = [
×
NEW
217
            '@extension' => $extensionName,
×
NEW
218
            '@controller' => $controllerName,
×
NEW
219
            '@action' => $actionName,
×
NEW
220
        ];
×
221

NEW
222
        $this->addHiddenField('__referrer[@extension]', htmlspecialchars($extensionName));
×
NEW
223
        $this->addHiddenField('__referrer[@controller]', htmlspecialchars($controllerName));
×
NEW
224
        $this->addHiddenField('__referrer[@action]', htmlspecialchars($actionName));
×
NEW
225
        $this->addHiddenField('__referrer[arguments]', htmlspecialchars($this->hashService->appendHmac(base64_encode(serialize($request->getArguments())), HashScope::ReferringArguments->prefix())));
×
NEW
226
        $this->addHiddenField('__referrer[@request]', htmlspecialchars($this->hashService->appendHmac(json_encode($actionRequest), HashScope::ReferringRequest->prefix())));
×
227

NEW
228
        return '';
×
229
    }
230

231
    protected function addHiddenField(string $name, mixed $value): void
232
    {
NEW
233
        $tmp = [];
×
NEW
234
        $tmp['name'] = $name;
×
NEW
235
        $tmp['type'] = 'hidden';
×
NEW
236
        $tmp['value'] = $value;
×
NEW
237
        $this->data['elements'][] = $tmp;
×
238
    }
239
}
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