Completed
Push — middleware-wip ( 3d734d...55159c )
by Romain
02:44
created

getContentObjectSettings()   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 33
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 33
rs 8.8571
c 0
b 0
f 0
cc 1
eloc 18
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\State\MiddlewareState;
30
use Romm\Formz\Service\ContextService;
31
use Romm\Formz\Service\ExtensionService;
32
use Romm\Formz\Service\MessageService;
33
use Romm\Formz\Validation\DataObject\ValidatorDataObject;
34
use TYPO3\CMS\Core\Utility\GeneralUtility;
35
use TYPO3\CMS\Extbase\Configuration\ConfigurationManager;
36
use TYPO3\CMS\Extbase\Error\Error;
37
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
38
use TYPO3\CMS\Extbase\Mvc\RequestInterface;
39
use TYPO3\CMS\Extbase\Mvc\ResponseInterface;
40
use TYPO3\CMS\Extbase\Mvc\Web\Request;
41
use TYPO3\CMS\Extbase\Mvc\Web\Response;
42
use TYPO3\CMS\Extbase\Reflection\ObjectAccess;
43
use TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface;
44
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
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
     * @todo
228
     *
229
     * @return array
230
     */
231
    protected function getContentObjectSettings()
232
    {
233
        $requestData = $this->formObject->getRequestData();
234
235
        $referringRequest = $this->request->getReferringRequest();
236
237
        /** @var ConfigurationManager $configurationManager */
238
        $configurationManager = $this->objectManager->get(ConfigurationManager::class);
239
240
        $contentObjectBackup = $configurationManager->getContentObject();
241
//        $configurationBackup = $configurationManager->getConfiguration();
242
243
        $content = Core::get()->getDatabase()->exec_SELECTgetSingleRow(
244
            '*',
245
            $requestData->getContentObjectTable(),
246
            'uid=' . $requestData->getContentObjectUid()
247
        );
248
249
        $contentObject = new ContentObjectRenderer;
250
        $contentObject->start($content, $requestData->getContentObjectTable());
0 ignored issues
show
Bug introduced by
It seems like $content defined by \Romm\Formz\Core\Core::g...>getContentObjectUid()) on line 243 can also be of type boolean or null; however, TYPO3\CMS\Frontend\Conte...ObjectRenderer::start() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
251
        $configurationManager->setConfiguration([
252
            'extensionName' => $referringRequest->getControllerExtensionName(),
253
            'pluginName'    => $referringRequest->getPluginName()
254
        ]);
255
        $configurationManager->setContentObject($contentObject);
256
257
        $configuration = $configurationManager->getConfiguration(ConfigurationManager::CONFIGURATION_TYPE_SETTINGS);
258
259
        $configurationManager->setContentObject($contentObjectBackup);
260
//        $configurationManager->setConfiguration($configurationBackup);
261
262
        return $configuration;
263
    }
264
265
    /**
266
     * @todo
267
     */
268
    protected function invokeMiddlewares()
269
    {
270
        $controllerState = ControllerState::get();
271
        $controllerState->setData($this->request, $this->arguments, $this->getContentObjectSettings());
272
273
        /** @var MiddlewareState $middlewareState */
274
        $middlewareState = Core::instantiate(MiddlewareState::class, $this->formObject, $controllerState);
275
276
        $middlewareState->activateSingleFieldValidationContext();
277
        $middlewareState->run();
278
    }
279
280
    /**
281
     * @return Validation
282
     * @throws EntryNotFoundException
283
     * @throws InvalidConfigurationException
284
     */
285
    protected function getFieldValidation()
286
    {
287
        $validationResult = $this->formObject->getConfigurationValidationResult();
288
289
        if (true === $validationResult->hasErrors()) {
290
            throw InvalidConfigurationException::ajaxControllerInvalidFormConfiguration();
291
        }
292
293
        $formConfiguration = $this->formObject->getConfiguration();
294
295
        if (false === $formConfiguration->hasField($this->fieldName)) {
296
            throw EntryNotFoundException::ajaxControllerFieldNotFound($this->fieldName, $this->formObject);
297
        }
298
299
        $field = $formConfiguration->getField($this->fieldName);
300
301
        if (false === $field->hasValidation($this->validatorName)) {
302
            throw EntryNotFoundException::ajaxControllerValidationNotFoundForField($this->validatorName, $this->fieldName);
303
        }
304
305
        $fieldValidationConfiguration = $field->getValidationByName($this->validatorName);
306
307
        if (false === $fieldValidationConfiguration->doesUseAjax()) {
308
            throw InvalidConfigurationException::ajaxControllerAjaxValidationNotActivated($this->validatorName, $this->fieldName);
309
        }
310
311
        return $fieldValidationConfiguration;
312
    }
313
314
    /**
315
     * Fetches errors/warnings/notices in the result, and put them in the JSON
316
     * response.
317
     */
318
    protected function injectResultInResponse()
319
    {
320
        $validationName = $this->validation instanceof Validation
321
            ? $this->validation->getName()
322
            : 'default';
323
324
        $validationResult = MessageService::get()->sanitizeValidatorResult($this->result, $validationName);
325
326
        $result = [
327
            'success'  => !$this->result->hasErrors(),
328
            'data'     => $this->result->getData(),
329
            'messages' => [
330
                'errors'   => $this->formatMessages($validationResult->getErrors()),
331
                'warnings' => $this->formatMessages($validationResult->getWarnings()),
332
                'notices'  => $this->formatMessages($validationResult->getNotices())
333
            ]
334
        ];
335
336
        $this->setUpResponseResult($result);
337
    }
338
339
    /**
340
     * @param array $result
341
     */
342
    protected function setUpResponseResult(array $result)
343
    {
344
        $this->response->setHeader('Content-Type', 'application/json');
345
        $this->response->setContent(json_encode($result));
346
347
        Core::get()->getPageController()->setContentType('application/json');
348
    }
349
350
    /**
351
     * @param FormzMessageInterface[] $messages
352
     * @return array
353
     */
354
    protected function formatMessages(array $messages)
355
    {
356
        $sortedMessages = [];
357
358
        foreach ($messages as $message) {
359
            $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...
360
        }
361
362
        return $sortedMessages;
363
    }
364
365
    /**
366
     * Wrapper for unit tests.
367
     *
368
     * @param RequestInterface  $request
369
     * @param ResponseInterface $response
370
     */
371
    protected function processRequestParent(RequestInterface $request, ResponseInterface $response)
372
    {
373
        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...
374
    }
375
376
    /**
377
     * Wrapper for unit tests.
378
     */
379
    protected function initializeActionMethodValidatorsParent()
380
    {
381
        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...
382
    }
383
384
    /**
385
     * Used in unit testing.
386
     *
387
     * @param bool $flag
388
     */
389
    public function setProtectedRequestMode($flag)
390
    {
391
        $this->protectedRequestMode = (bool)$flag;
392
    }
393
394
    /**
395
     * @param Exception $exception
396
     * @return string
397
     */
398
    protected function getDebugMessageForException(Exception $exception)
399
    {
400
        return 'Debug mode – ' . $exception->getMessage();
401
    }
402
403
    /**
404
     * @return FormInterface
405
     * @throws MissingArgumentException
406
     */
407
    protected function getForm()
408
    {
409
        return $this->arguments->getArgument($this->formName)->getValue();
410
    }
411
412
    /**
413
     * @return FormObject
414
     */
415
    protected function getFormObject()
416
    {
417
        /** @var FormObjectFactory $formObjectFactory */
418
        $formObjectFactory = Core::instantiate(FormObjectFactory::class);
419
420
        return $formObjectFactory->getInstanceFromClassName($this->formClassName, $this->formName);
421
    }
422
423
    /**
424
     * Wrapper for unit tests.
425
     *
426
     * @return Request
427
     */
428
    protected function getRequest()
429
    {
430
        return $this->request;
431
    }
432
}
433