Completed
Push — middleware-wip ( 55159c...844477 )
by Romain
02:34
created

getContentObjectSettings()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 12
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 8
nc 1
nop 0
1
<?php
2
/*
3
 * 2017 Romain CANON <[email protected]>
4
 *
5
 * This file is part of the TYPO3 FormZ project.
6
 * It is free software; you can redistribute it and/or modify it
7
 * under the terms of the GNU General Public License, either
8
 * version 3 of the License, or any later version.
9
 *
10
 * For the full copyright and license information, see:
11
 * http://www.gnu.org/licenses/gpl-3.0.html
12
 */
13
14
namespace Romm\Formz\Controller;
15
16
use Exception;
17
use Romm\Formz\Configuration\Form\Field\Validation\Validation;
18
use Romm\Formz\Core\Core;
19
use Romm\Formz\Error\AjaxResult;
20
use Romm\Formz\Error\FormzMessageInterface;
21
use Romm\Formz\Exceptions\ClassNotFoundException;
22
use Romm\Formz\Exceptions\EntryNotFoundException;
23
use Romm\Formz\Exceptions\InvalidArgumentTypeException;
24
use Romm\Formz\Exceptions\InvalidConfigurationException;
25
use Romm\Formz\Exceptions\MissingArgumentException;
26
use Romm\Formz\Form\FormInterface;
27
use Romm\Formz\Form\FormObject;
28
use Romm\Formz\Form\FormObjectFactory;
29
use Romm\Formz\Middleware\Request\Exception\StopPropagationException;
30
use Romm\Formz\Middleware\State\MiddlewareState;
31
use Romm\Formz\Service\ContentObjectService;
32
use Romm\Formz\Service\ContextService;
33
use Romm\Formz\Service\ExtensionService;
34
use Romm\Formz\Service\MessageService;
35
use Romm\Formz\Validation\DataObject\ValidatorDataObject;
36
use TYPO3\CMS\Core\Utility\GeneralUtility;
37
use TYPO3\CMS\Extbase\Error\Error;
38
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
39
use TYPO3\CMS\Extbase\Mvc\RequestInterface;
40
use TYPO3\CMS\Extbase\Mvc\ResponseInterface;
41
use TYPO3\CMS\Extbase\Mvc\Web\Request;
42
use TYPO3\CMS\Extbase\Mvc\Web\Response;
43
use TYPO3\CMS\Extbase\Reflection\ObjectAccess;
44
use TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface;
45
46
class AjaxValidationController extends ActionController
47
{
48
    const DEFAULT_ERROR_MESSAGE_KEY = 'default_error_message';
49
50
    /**
51
     * @var Request
52
     */
53
    protected $request;
54
55
    /**
56
     * @var Response
57
     */
58
    protected $response;
59
60
    /**
61
     * @var bool
62
     */
63
    protected $protectedRequestMode = true;
64
65
    /**
66
     * @var string
67
     */
68
    protected $formClassName;
69
70
    /**
71
     * @var string
72
     */
73
    protected $formName;
74
75
    /**
76
     * @var string
77
     */
78
    protected $fieldName;
79
80
    /**
81
     * @var string
82
     */
83
    protected $validatorName;
84
85
    /**
86
     * @var FormInterface
87
     */
88
    protected $form;
89
90
    /**
91
     * @var FormObject
92
     */
93
    protected $formObject;
94
95
    /**
96
     * @var AjaxResult
97
     */
98
    protected $result;
99
100
    /**
101
     * @var Validation
102
     */
103
    protected $validation;
104
105
    /**
106
     * The only accepted method for the request is `POST`.
107
     */
108
    public function initializeAction()
109
    {
110
        if ($this->request->getMethod() !== 'POST') {
111
            $this->throwStatus(400);
112
        }
113
    }
114
115
    /**
116
     * Will process the request, but also prevent any external message to be
117
     * displayed, and catch any exception that could occur during the
118
     * validation.
119
     *
120
     * @param RequestInterface  $request
121
     * @param ResponseInterface $response
122
     * @throws Exception
123
     */
124
    public function processRequest(RequestInterface $request, ResponseInterface $response)
125
    {
126
        $this->result = new AjaxResult;
127
128
        try {
129
            $this->processRequestParent($request, $response);
130
        } catch (Exception $exception) {
131
            if (false === $this->protectedRequestMode) {
132
                throw $exception;
133
            }
134
135
            $this->result->clear();
136
137
            $errorMessage = ExtensionService::get()->isInDebugMode()
138
                ? $this->getDebugMessageForException($exception)
139
                : ContextService::get()->translate(self::DEFAULT_ERROR_MESSAGE_KEY);
140
141
            $error = new Error($errorMessage, 1490176818);
142
            $this->result->addError($error);
143
            $this->result->setData('errorCode', $exception->getCode());
144
        }
145
146
        // Cleaning every external message.
147
        ob_clean();
148
149
        $this->injectResultInResponse();
150
    }
151
152
    /**
153
     * Will take care of adding a new argument to the request, based on the form
154
     * name and the form class name found in the request arguments.
155
     */
156
    protected function initializeActionMethodValidators()
157
    {
158
        $this->initializeActionMethodValidatorsParent();
159
160
        $request = $this->getRequest();
161
162
        if (false === $request->hasArgument('name')) {
163
            throw MissingArgumentException::ajaxControllerNameArgumentNotSet();
164
        }
165
166
        if (false === $request->hasArgument('className')) {
167
            throw MissingArgumentException::ajaxControllerClassNameArgumentNotSet();
168
        }
169
170
        $className = $request->getArgument('className');
171
172
        if (false === class_exists($className)) {
173
            throw ClassNotFoundException::ajaxControllerFormClassNameNotFound($className);
174
        }
175
176
        if (false === in_array(FormInterface::class, class_implements($className))) {
177
            throw InvalidArgumentTypeException::ajaxControllerWrongFormType($className);
178
        }
179
180
        $this->arguments->addNewArgument($request->getArgument('name'), $className, true);
181
    }
182
183
    /**
184
     * Main action that will process the field validation.
185
     *
186
     * @param string $name
187
     * @param string $className
188
     * @param string $fieldName
189
     * @param string $validatorName
190
     * @param string $formzData
191
     */
192
    public function runAction($name, $className, $fieldName, $validatorName, $formzData = null)
193
    {
194
        $this->formName = $name;
195
        $this->formClassName = $className;
196
        $this->fieldName = $fieldName;
197
        $this->validatorName = $validatorName;
198
        $this->form = $this->getForm();
199
200
        $this->formObject = $this->getFormObject();
201
        $this->formObject->setForm($this->form);
202
203
        if ($formzData) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $formzData of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
204
            $this->formObject->getRequestData()->fillFromHash($formzData);
205
        }
206
207
        $this->invokeMiddlewares();
208
209
        $this->validation = $this->getFieldValidation();
210
211
        $validatorDataObject = new ValidatorDataObject($this->formObject, $this->validation);
212
213
        /** @var ValidatorInterface $validator */
214
        $validator = GeneralUtility::makeInstance(
215
            $this->validation->getClassName(),
216
            $this->validation->getOptions(),
217
            $validatorDataObject
218
        );
219
220
        $fieldValue = ObjectAccess::getProperty($this->form, $this->fieldName);
221
        $result = $validator->validate($fieldValue);
222
223
        $this->result->merge($result);
224
    }
225
226
    /**
227
     * Will fetch the settings of the content object that was used to render the
228
     * form calling this controller.
229
     *
230
     * @return array
231
     */
232
    protected function getContentObjectSettings()
233
    {
234
        $requestData = $this->formObject->getRequestData();
235
        $referringRequest = $this->request->getReferringRequest();
236
237
        return ContentObjectService::get()->getContentObjectSettings(
238
            $requestData->getContentObjectTable(),
239
            $requestData->getContentObjectUid(),
240
            $referringRequest->getControllerExtensionName(),
241
            $referringRequest->getPluginName()
242
        );
243
    }
244
245
    /**
246
     * Will call all middlewares of the form.
247
     *
248
     * Note that the "single field validation context" is activated, meaning
249
     * some middlewares wont be called.
250
     *
251
     * @see \Romm\Formz\Middleware\State\RemoveFromSingleFieldValidationContext
252
     */
253
    protected function invokeMiddlewares()
254
    {
255
        try {
256
            $controllerState = ControllerState::get();
257
            $controllerState->setData($this->request, $this->arguments, $this->getContentObjectSettings());
258
259
            /** @var MiddlewareState $middlewareState */
260
            $middlewareState = Core::instantiate(MiddlewareState::class, $this->formObject, $controllerState);
261
262
            $middlewareState->activateSingleFieldValidationContext();
263
            $middlewareState->run();
264
        } catch (StopPropagationException $exception) {
265
            // @todo exception if forward/redirect?
266
        }
267
    }
268
269
    /**
270
     * @return Validation
271
     * @throws EntryNotFoundException
272
     * @throws InvalidConfigurationException
273
     */
274
    protected function getFieldValidation()
275
    {
276
        $validationResult = $this->formObject->getConfigurationValidationResult();
277
278
        if (true === $validationResult->hasErrors()) {
279
            throw InvalidConfigurationException::ajaxControllerInvalidFormConfiguration();
280
        }
281
282
        $formConfiguration = $this->formObject->getConfiguration();
283
284
        if (false === $formConfiguration->hasField($this->fieldName)) {
285
            throw EntryNotFoundException::ajaxControllerFieldNotFound($this->fieldName, $this->formObject);
286
        }
287
288
        $field = $formConfiguration->getField($this->fieldName);
289
290
        if (false === $field->hasValidation($this->validatorName)) {
291
            throw EntryNotFoundException::ajaxControllerValidationNotFoundForField($this->validatorName, $this->fieldName);
292
        }
293
294
        $fieldValidationConfiguration = $field->getValidationByName($this->validatorName);
295
296
        if (false === $fieldValidationConfiguration->doesUseAjax()) {
297
            throw InvalidConfigurationException::ajaxControllerAjaxValidationNotActivated($this->validatorName, $this->fieldName);
298
        }
299
300
        return $fieldValidationConfiguration;
301
    }
302
303
    /**
304
     * Fetches errors/warnings/notices in the result, and put them in the JSON
305
     * response.
306
     */
307
    protected function injectResultInResponse()
308
    {
309
        $validationName = $this->validation instanceof Validation
310
            ? $this->validation->getName()
311
            : 'default';
312
313
        $validationResult = MessageService::get()->sanitizeValidatorResult($this->result, $validationName);
314
315
        $result = [
316
            'success'  => !$this->result->hasErrors(),
317
            'data'     => $this->result->getData(),
318
            'messages' => [
319
                'errors'   => $this->formatMessages($validationResult->getErrors()),
320
                'warnings' => $this->formatMessages($validationResult->getWarnings()),
321
                'notices'  => $this->formatMessages($validationResult->getNotices())
322
            ]
323
        ];
324
325
        $this->setUpResponseResult($result);
326
    }
327
328
    /**
329
     * @param array $result
330
     */
331
    protected function setUpResponseResult(array $result)
332
    {
333
        $this->response->setHeader('Content-Type', 'application/json');
334
        $this->response->setContent(json_encode($result));
335
336
        Core::get()->getPageController()->setContentType('application/json');
337
    }
338
339
    /**
340
     * @param FormzMessageInterface[] $messages
341
     * @return array
342
     */
343
    protected function formatMessages(array $messages)
344
    {
345
        $sortedMessages = [];
346
347
        foreach ($messages as $message) {
348
            $sortedMessages[$message->getMessageKey()] = $message->getMessage();
0 ignored issues
show
Bug introduced by
The method getMessage() does not exist on Romm\Formz\Error\FormzMessageInterface. Did you maybe mean getMessageKey()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
349
        }
350
351
        return $sortedMessages;
352
    }
353
354
    /**
355
     * Wrapper for unit tests.
356
     *
357
     * @param RequestInterface  $request
358
     * @param ResponseInterface $response
359
     */
360
    protected function processRequestParent(RequestInterface $request, ResponseInterface $response)
361
    {
362
        parent::processRequest($request, $response);
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (processRequest() instead of processRequestParent()). Are you sure this is correct? If so, you might want to change this to $this->processRequest().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
363
    }
364
365
    /**
366
     * Wrapper for unit tests.
367
     */
368
    protected function initializeActionMethodValidatorsParent()
369
    {
370
        parent::initializeActionMethodValidators();
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (initializeActionMethodValidators() instead of initializeActionMethodValidatorsParent()). Are you sure this is correct? If so, you might want to change this to $this->initializeActionMethodValidators().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
371
    }
372
373
    /**
374
     * Used in unit testing.
375
     *
376
     * @param bool $flag
377
     */
378
    public function setProtectedRequestMode($flag)
379
    {
380
        $this->protectedRequestMode = (bool)$flag;
381
    }
382
383
    /**
384
     * @param Exception $exception
385
     * @return string
386
     */
387
    protected function getDebugMessageForException(Exception $exception)
388
    {
389
        return 'Debug mode – ' . $exception->getMessage();
390
    }
391
392
    /**
393
     * @return FormInterface
394
     * @throws MissingArgumentException
395
     */
396
    protected function getForm()
397
    {
398
        return $this->arguments->getArgument($this->formName)->getValue();
399
    }
400
401
    /**
402
     * @return FormObject
403
     */
404
    protected function getFormObject()
405
    {
406
        /** @var FormObjectFactory $formObjectFactory */
407
        $formObjectFactory = Core::instantiate(FormObjectFactory::class);
408
409
        return $formObjectFactory->getInstanceFromClassName($this->formClassName, $this->formName);
410
    }
411
412
    /**
413
     * Wrapper for unit tests.
414
     *
415
     * @return Request
416
     */
417
    protected function getRequest()
418
    {
419
        return $this->request;
420
    }
421
}
422