Completed
Push — unit-tests-conditions ( a23049...dc9eee )
by Romain
02:30
created

AjaxValidationController::formatMessages()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 10
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 5
nc 2
nop 1
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 Romm\Formz\Configuration\Form\Field\Validation\Validation;
17
use Romm\Formz\Configuration\Form\Form;
18
use Romm\Formz\Core\Core;
19
use Romm\Formz\Error\FormzMessageInterface;
20
use Romm\Formz\Exceptions\EntryNotFoundException;
21
use Romm\Formz\Exceptions\InvalidConfigurationException;
22
use Romm\Formz\Exceptions\MissingArgumentException;
23
use Romm\Formz\Form\FormInterface;
24
use Romm\Formz\Form\FormObject;
25
use Romm\Formz\Form\FormObjectFactory;
26
use Romm\Formz\Service\ContextService;
27
use Romm\Formz\Service\ExtensionService;
28
use Romm\Formz\Service\MessageService;
29
use Romm\Formz\Validation\DataObject\ValidatorDataObject;
30
use TYPO3\CMS\Core\Utility\GeneralUtility;
31
use TYPO3\CMS\Extbase\Error\Result;
32
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
33
use TYPO3\CMS\Extbase\Mvc\View\JsonView;
34
use TYPO3\CMS\Extbase\Mvc\Web\Request;
35
use TYPO3\CMS\Extbase\Property\PropertyMapper;
36
use TYPO3\CMS\Extbase\Reflection\ObjectAccess;
37
use TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface;
38
39
class AjaxValidationController extends ActionController
40
{
41
    const ARGUMENT_FORM_CLASS_NAME = 'formClassName';
42
    const ARGUMENT_FORM_NAME = 'formName';
43
    const ARGUMENT_FORM = 'form';
44
    const ARGUMENT_FIELD_NAME = 'fieldName';
45
    const ARGUMENT_VALIDATOR_NAME = 'validatorName';
46
47
    const DEFAULT_ERROR_MESSAGE_KEY = 'default_error_message';
48
49
    /**
50
     * @var array
51
     */
52
    public static $requiredArguments = [
53
        self::ARGUMENT_FORM_CLASS_NAME,
54
        self::ARGUMENT_FORM_NAME,
55
        self::ARGUMENT_FORM,
56
        self::ARGUMENT_FIELD_NAME,
57
        self::ARGUMENT_VALIDATOR_NAME
58
    ];
59
60
    /**
61
     * @var JsonView
62
     */
63
    protected $view;
64
65
    /**
66
     * @var string
67
     */
68
    protected $defaultViewObjectName = JsonView::class;
69
70
    /**
71
     * @var Request
72
     */
73
    protected $request;
74
75
    /**
76
     * @var bool
77
     */
78
    protected $protectedRequestMode = true;
79
80
    /**
81
     * @var string
82
     */
83
    protected $formClassName;
84
85
    /**
86
     * @var string
87
     */
88
    protected $formName;
89
90
    /**
91
     * @var array
92
     */
93
    protected $form;
94
95
    /**
96
     * @var string
97
     */
98
    protected $fieldName;
99
100
    /**
101
     * @var string
102
     */
103
    protected $validatorName;
104
105
    /**
106
     * @var FormObject
107
     */
108
    protected $formObject;
109
110
    /**
111
     * The only accepted method for the request is `POST`.
112
     */
113
    public function initializeAction()
114
    {
115
        if ($this->request->getMethod() !== 'POST') {
116
            $this->throwStatus(400);
117
        }
118
    }
119
120
    public function getView()
121
    {
122
        return $this->view;
123
    }
124
125
    /**
126
     * Main action that will render the validation result.
127
     */
128
    public function runAction()
129
    {
130
        $result = ($this->protectedRequestMode)
131
            ? $this->getProtectedRequestResult()
132
            : $this->getRequestResult();
133
134
        $this->view->setVariablesToRender(['result']);
135
        $this->view->assign('result', $result);
136
    }
137
138
    /**
139
     * @param bool $flag
140
     */
141
    public function setProtectedRequestMode($flag)
142
    {
143
        $this->protectedRequestMode = (bool)$flag;
144
    }
145
146
    /**
147
     * Will fetch the result and prevent any exception or external message to be
148
     * displayed.
149
     *
150
     * @return array
151
     */
152
    protected function getProtectedRequestResult()
153
    {
154
        // Default technical error result if the function can not be reached.
155
        $result = [
156
            'success' => false,
157
            'messages' => [
158
                'errors' => ['default' => ContextService::get()->translate(self::DEFAULT_ERROR_MESSAGE_KEY)]
159
            ]
160
        ];
161
162
        // We prevent any external message to be displayed here.
163
        ob_start();
164
165
        try {
166
            $result = $this->getRequestResult();
167
        } catch (\Exception $exception) {
168
            $result['data'] = ['errorCode' => $exception->getCode()];
169
170
            if (ExtensionService::get()->isInDebugMode()) {
171
                $result['messages']['errors']['default'] = $this->getDebugMessageForException($exception);
172
            }
173
        }
174
175
        ob_end_clean();
176
177
        return $result;
178
    }
179
180
    /**
181
     * Will get the result of the validation for this Ajax request.
182
     *
183
     * If any error is found, an exception is thrown.
184
     *
185
     * @return array
186
     */
187
    protected function getRequestResult()
188
    {
189
        $this->initializeArguments();
190
191
        $this->formObject = $this->getFormObject();
192
        $this->checkConfigurationValidationResult();
193
        $validation = $this->getFieldValidation();
194
        $form = $this->buildFormObject();
195
        $fieldValue = ObjectAccess::getProperty($form, $this->fieldName);
196
        $validatorDataObject = new ValidatorDataObject($this->formObject, $form, $validation);
197
198
        /** @var ValidatorInterface $validator */
199
        $validator = GeneralUtility::makeInstance(
200
            $validation->getClassName(),
201
            $validation->getOptions(),
202
            $validatorDataObject
203
        );
204
205
        $result = $validator->validate($fieldValue);
206
        $result = MessageService::get()->sanitizeValidatorResult($result, $validation);
207
208
        return $this->convertResultToJson($result);
209
    }
210
211
    /**
212
     * Initializes all arguments for the request, and returns an array
213
     * containing the missing arguments.
214
     */
215
    protected function initializeArguments()
216
    {
217
        $argumentsMissing = [];
218
219
        foreach (self::$requiredArguments as $argument) {
220
            $argumentValue = $this->getArgument($argument);
221
222
            if ($argumentValue) {
223
                $this->$argument = $argumentValue;
224
            } else {
225
                $argumentsMissing[] = $argument;
226
            }
227
        }
228
229
        if (false === empty($argumentsMissing)) {
230
            throw new MissingArgumentException(
231
                'One or more arguments are missing in the request: "' . implode('", "', $argumentsMissing) . '".',
232
                1487673983
233
            );
234
        }
235
    }
236
237
    /**
238
     * @return FormObject
239
     */
240
    protected function getFormObject()
241
    {
242
        /** @var FormObjectFactory $formObjectFactory */
243
        $formObjectFactory = Core::instantiate(FormObjectFactory::class);
244
245
        return $formObjectFactory->getInstanceFromClassName($this->formClassName, $this->formName);
246
    }
247
248
    /**
249
     * @throws InvalidConfigurationException
250
     */
251
    protected function checkConfigurationValidationResult()
252
    {
253
        $validationResult = $this->formObject->getConfigurationValidationResult();
254
255
        if (true === $validationResult->hasErrors()) {
256
            throw new InvalidConfigurationException(
257
                'The form configuration contains errors.',
258
                1487671395
259
            );
260
        }
261
    }
262
263
    /**
264
     * @return Validation
265
     * @throws EntryNotFoundException
266
     * @throws InvalidConfigurationException
267
     */
268
    protected function getFieldValidation()
269
    {
270
        $formConfiguration = $this->getFormConfiguration($this->formObject);
271
        $field = $formConfiguration->getField($this->fieldName);
272
273
        if (false === $field->hasValidation($this->validatorName)) {
274
            throw new EntryNotFoundException(
275
                'The field "' . $this->fieldName . '" does not have a rule "' . $this->validatorName . '".',
276
                1487672956
277
            );
278
        }
279
280
        $fieldValidationConfiguration = $field->getValidationByName($this->validatorName);
281
282
        if (false === $fieldValidationConfiguration->doesUseAjax()) {
283
            throw new InvalidConfigurationException(
284
                'The validation "' . $this->validatorName . '" of the field "' . $this->fieldName . '" is not configured to work with Ajax. Please add the option "useAjax".',
285
                1487673434
286
            );
287
        }
288
289
        return $fieldValidationConfiguration;
290
    }
291
292
    /**
293
     * @param FormObject $formObject
294
     * @return Form
295
     * @throws EntryNotFoundException
296
     */
297
    protected function getFormConfiguration(FormObject $formObject)
298
    {
299
        $formConfiguration = $formObject->getConfiguration();
300
301
        if (false === $formConfiguration->hasField($this->fieldName)) {
302
            throw new EntryNotFoundException(
303
                'The field "' . $this->fieldName . '" was not found in the form "' . $this->formName . '" with class "' . $this->formClassName . '".',
304
                1487671603
305
            );
306
        }
307
308
        return $formConfiguration;
309
    }
310
311
    /**
312
     * Will build and fill an object with a form sent value.
313
     *
314
     * @return FormInterface
315
     */
316
    protected function buildFormObject()
317
    {
318
        $values = $this->cleanValuesFromUrl($this->form);
319
320
        return $this->getPropertyMapper()->convert($values, $this->formClassName);
321
    }
322
323
    /**
324
     * Will convert the result of the function called by this class in a JSON
325
     * string.
326
     *
327
     * @param Result $result
328
     * @return array
329
     */
330
    protected function convertResultToJson(Result $result)
331
    {
332
        $messages = [];
333
334
        if ($result->hasErrors()) {
335
            $messages['errors'] = $this->formatMessages($result->getErrors());
0 ignored issues
show
Documentation introduced by
$result->getErrors() is of type array<integer,object<TYP...S\Extbase\Error\Error>>, but the function expects a array<integer,object<Rom...FormzMessageInterface>>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
336
        }
337
338
        if ($result->hasWarnings()) {
339
            $messages['warnings'] = $this->formatMessages($result->getWarnings());
0 ignored issues
show
Documentation introduced by
$result->getWarnings() is of type array<integer,object<TYP...Extbase\Error\Warning>>, but the function expects a array<integer,object<Rom...FormzMessageInterface>>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
340
        }
341
342
        if ($result->hasNotices()) {
343
            $messages['notices'] = $this->formatMessages($result->getNotices());
0 ignored issues
show
Documentation introduced by
$result->getNotices() is of type array<integer,object<TYP...\Extbase\Error\Notice>>, but the function expects a array<integer,object<Rom...FormzMessageInterface>>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
344
        }
345
346
        return [
347
            'success' => !$result->hasErrors(),
348
            'messages' => $messages
349
        ];
350
    }
351
352
    /**
353
     * @param FormzMessageInterface[] $messages
354
     * @return array
355
     */
356
    protected function formatMessages(array $messages)
357
    {
358
        $sortedMessages = [];
359
360
        foreach ($messages as $message) {
361
            $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...
362
        }
363
364
        return $sortedMessages;
365
    }
366
367
    /**
368
     * @param \Exception $exception
369
     * @return string
370
     */
371
    protected function getDebugMessageForException(\Exception $exception)
372
    {
373
        return 'Debug mode – ' . $exception->getMessage();
374
    }
375
376
    /**
377
     * @param string $name
378
     * @return mixed
379
     */
380
    protected function getArgument($name)
381
    {
382
        return GeneralUtility::_GP($name);
383
    }
384
385
    /**
386
     * Will clean the string filled with form values sent with Ajax.
387
     *
388
     * @param array $values
389
     * @return array
390
     */
391
    protected function cleanValuesFromUrl($values)
392
    {
393
        // Cleaning the given form values.
394
        $values = reset($values);
395
        unset($values['__referrer']);
396
        unset($values['__trustedProperties']);
397
398
        return reset($values);
399
    }
400
401
    /**
402
     * @return PropertyMapper
403
     */
404
    protected function getPropertyMapper()
405
    {
406
        /** @var PropertyMapper $propertyMapper */
407
        $propertyMapper = Core::instantiate(PropertyMapper::class);
408
409
        return $propertyMapper;
410
    }
411
}
412