[CLEANUP] The correct case must be used for standard PHP types in phpdoc
[Packages/TYPO3.CMS.git] / typo3 / sysext / extbase / Classes / Validation / ValidatorResolver.php
1 <?php
2 namespace TYPO3\CMS\Extbase\Validation;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use TYPO3\CMS\Core\Utility\ClassNamingUtility;
18 use TYPO3\CMS\Core\Utility\GeneralUtility;
19 use TYPO3\CMS\Extbase\Utility\TypeHandlingUtility;
20 use TYPO3\CMS\Extbase\Validation\Exception\NoSuchValidatorException;
21 use TYPO3\CMS\Extbase\Validation\Validator\ConjunctionValidator;
22
23 /**
24 * Validator resolver to automatically find an appropriate validator for a given subject
25 */
26 class ValidatorResolver implements \TYPO3\CMS\Core\SingletonInterface
27 {
28 /**
29 * Match validator names and options
30 * @todo: adjust [a-z0-9_:.\\\\] once Tx_Extbase_Foo syntax is outdated.
31 *
32 * @var string
33 */
34 const PATTERN_MATCH_VALIDATORS = '/
35 (?:^|,\s*)
36 (?P<validatorName>[a-z0-9_:.\\\\]+)
37 \s*
38 (?:\(
39 (?P<validatorOptions>(?:\s*[a-z0-9]+\s*=\s*(?:
40 "(?:\\\\"|[^"])*"
41 |\'(?:\\\\\'|[^\'])*\'
42 |(?:\s|[^,"\']*)
43 )(?:\s|,)*)*)
44 \))?
45 /ixS';
46
47 /**
48 * Match validator options (to parse actual options)
49 * @var string
50 */
51 const PATTERN_MATCH_VALIDATOROPTIONS = '/
52 \s*
53 (?P<optionName>[a-z0-9]+)
54 \s*=\s*
55 (?P<optionValue>
56 "(?:\\\\"|[^"])*"
57 |\'(?:\\\\\'|[^\'])*\'
58 |(?:\s|[^,"\']*)
59 )
60 /ixS';
61
62 /**
63 * @var \TYPO3\CMS\Extbase\Object\ObjectManagerInterface
64 */
65 protected $objectManager;
66
67 /**
68 * @var \TYPO3\CMS\Extbase\Reflection\ReflectionService
69 */
70 protected $reflectionService;
71
72 /**
73 * @var array
74 */
75 protected $baseValidatorConjunctions = [];
76
77 /**
78 * @param \TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager
79 */
80 public function injectObjectManager(\TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager)
81 {
82 $this->objectManager = $objectManager;
83 }
84
85 /**
86 * @param \TYPO3\CMS\Extbase\Reflection\ReflectionService $reflectionService
87 */
88 public function injectReflectionService(\TYPO3\CMS\Extbase\Reflection\ReflectionService $reflectionService)
89 {
90 $this->reflectionService = $reflectionService;
91 }
92
93 /**
94 * Get a validator for a given data type. Returns a validator implementing
95 * the \TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface or NULL if no validator
96 * could be resolved.
97 *
98 * @param string $validatorType Either one of the built-in data types or fully qualified validator class name
99 * @param array $validatorOptions Options to be passed to the validator
100 * @return \TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface Validator or NULL if none found.
101 */
102 public function createValidator($validatorType, array $validatorOptions = [])
103 {
104 try {
105 /**
106 * @todo remove throwing Exceptions in resolveValidatorObjectName
107 */
108 $validatorObjectName = $this->resolveValidatorObjectName($validatorType);
109
110 $validator = $this->objectManager->get($validatorObjectName, $validatorOptions);
111
112 if (!($validator instanceof \TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface)) {
113 throw new Exception\NoSuchValidatorException('The validator "' . $validatorObjectName . '" does not implement TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface!', 1300694875);
114 }
115
116 return $validator;
117 } catch (NoSuchValidatorException $e) {
118 GeneralUtility::devLog($e->getMessage(), 'extbase', GeneralUtility::SYSLOG_SEVERITY_INFO);
119 return null;
120 }
121 }
122
123 /**
124 * Resolves and returns the base validator conjunction for the given data type.
125 *
126 * If no validator could be resolved (which usually means that no validation is necessary),
127 * NULL is returned.
128 *
129 * @param string $targetClassName The data type to search a validator for. Usually the fully qualified object name
130 * @return ConjunctionValidator The validator conjunction or NULL
131 */
132 public function getBaseValidatorConjunction($targetClassName)
133 {
134 if (!array_key_exists($targetClassName, $this->baseValidatorConjunctions)) {
135 $this->buildBaseValidatorConjunction($targetClassName, $targetClassName);
136 }
137
138 return $this->baseValidatorConjunctions[$targetClassName];
139 }
140
141 /**
142 * Detects and registers any validators for arguments:
143 * - by the data type specified in the param annotations
144 * - additional validators specified in the validate annotations of a method
145 *
146 * @param string $className
147 * @param string $methodName
148 * @param array $methodParameters Optional pre-compiled array of method parameters
149 * @param array $methodValidateAnnotations Optional pre-compiled array of validate annotations (as array)
150 * @return ConjunctionValidator[] An Array of ValidatorConjunctions for each method parameters.
151 * @throws \TYPO3\CMS\Extbase\Validation\Exception\InvalidValidationConfigurationException
152 * @throws \TYPO3\CMS\Extbase\Validation\Exception\NoSuchValidatorException
153 * @throws \TYPO3\CMS\Extbase\Validation\Exception\InvalidTypeHintException
154 */
155 public function buildMethodArgumentsValidatorConjunctions($className, $methodName, array $methodParameters = null, array $methodValidateAnnotations = null)
156 {
157 /** @var ConjunctionValidator[] $validatorConjunctions */
158 $validatorConjunctions = [];
159
160 if ($methodParameters === null) {
161 $methodParameters = $this->reflectionService->getMethodParameters($className, $methodName);
162 }
163 if (empty($methodParameters)) {
164 return $validatorConjunctions;
165 }
166
167 foreach ($methodParameters as $parameterName => $methodParameter) {
168 /** @var ConjunctionValidator $validatorConjunction */
169 $validatorConjunction = $this->createValidator(ConjunctionValidator::class);
170
171 if (!array_key_exists('type', $methodParameter)) {
172 throw new Exception\InvalidTypeHintException('Missing type information, probably no @param annotation for parameter "$' . $parameterName . '" in ' . $className . '->' . $methodName . '()', 1281962564);
173 }
174
175 // @todo: remove check for old underscore model name syntax once it's possible
176 if (strpbrk($methodParameter['type'], '_\\') === false) {
177 $typeValidator = $this->createValidator($methodParameter['type']);
178 } else {
179 $typeValidator = null;
180 }
181
182 if ($typeValidator !== null) {
183 $validatorConjunction->addValidator($typeValidator);
184 }
185 $validatorConjunctions[$parameterName] = $validatorConjunction;
186 }
187
188 if ($methodValidateAnnotations === null) {
189 $validateAnnotations = $this->getMethodValidateAnnotations($className, $methodName);
190 $methodValidateAnnotations = array_map(function ($validateAnnotation) {
191 return [
192 'type' => $validateAnnotation['validatorName'],
193 'options' => $validateAnnotation['validatorOptions'],
194 'argumentName' => $validateAnnotation['argumentName'],
195 ];
196 }, $validateAnnotations);
197 }
198
199 foreach ($methodValidateAnnotations as $annotationParameters) {
200 $newValidator = $this->createValidator($annotationParameters['type'], $annotationParameters['options']);
201 if ($newValidator === null) {
202 throw new Exception\NoSuchValidatorException('Invalid validate annotation in ' . $className . '->' . $methodName . '(): Could not resolve class name for validator "' . $annotationParameters['type'] . '".', 1239853109);
203 }
204 if (isset($validatorConjunctions[$annotationParameters['argumentName']])) {
205 $validatorConjunctions[$annotationParameters['argumentName']]->addValidator($newValidator);
206 } elseif (strpos($annotationParameters['argumentName'], '.') !== false) {
207 $objectPath = explode('.', $annotationParameters['argumentName']);
208 $argumentName = array_shift($objectPath);
209 $validatorConjunctions[$argumentName]->addValidator($this->buildSubObjectValidator($objectPath, $newValidator));
210 } else {
211 throw new Exception\InvalidValidationConfigurationException('Invalid validate annotation in ' . $className . '->' . $methodName . '(): Validator specified for argument name "' . $annotationParameters['argumentName'] . '", but this argument does not exist.', 1253172726);
212 }
213 }
214
215 return $validatorConjunctions;
216 }
217
218 /**
219 * Builds a chain of nested object validators by specification of the given
220 * object path.
221 *
222 * @param array $objectPath The object path
223 * @param \TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface $propertyValidator The validator which should be added to the property specified by objectPath
224 * @return \TYPO3\CMS\Extbase\Validation\Validator\GenericObjectValidator
225 */
226 protected function buildSubObjectValidator(array $objectPath, \TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface $propertyValidator)
227 {
228 $rootObjectValidator = $this->objectManager->get(\TYPO3\CMS\Extbase\Validation\Validator\GenericObjectValidator::class, []);
229 $parentObjectValidator = $rootObjectValidator;
230
231 while (count($objectPath) > 1) {
232 $subObjectValidator = $this->objectManager->get(\TYPO3\CMS\Extbase\Validation\Validator\GenericObjectValidator::class, []);
233 $subPropertyName = array_shift($objectPath);
234 $parentObjectValidator->addPropertyValidator($subPropertyName, $subObjectValidator);
235 $parentObjectValidator = $subObjectValidator;
236 }
237
238 $parentObjectValidator->addPropertyValidator(array_shift($objectPath), $propertyValidator);
239
240 return $rootObjectValidator;
241 }
242
243 /**
244 * Builds a base validator conjunction for the given data type.
245 *
246 * The base validation rules are those which were declared directly in a class (typically
247 * a model) through some validate annotations on properties.
248 *
249 * If a property holds a class for which a base validator exists, that property will be
250 * checked as well, regardless of a validate annotation
251 *
252 * Additionally, if a custom validator was defined for the class in question, it will be added
253 * to the end of the conjunction. A custom validator is found if it follows the naming convention
254 * "Replace '\Model\' by '\Validator\' and append 'Validator'".
255 *
256 * Example: $targetClassName is TYPO3\Foo\Domain\Model\Quux, then the validator will be found if it has the
257 * name TYPO3\Foo\Domain\Validator\QuuxValidator
258 *
259 * @param string $indexKey The key to use as index in $this->baseValidatorConjunctions; calculated from target class name and validation groups
260 * @param string $targetClassName The data type to build the validation conjunction for. Needs to be the fully qualified class name.
261 * @param array $validationGroups The validation groups to build the validator for
262 * @throws \TYPO3\CMS\Extbase\Validation\Exception\NoSuchValidatorException
263 * @throws \InvalidArgumentException
264 */
265 protected function buildBaseValidatorConjunction($indexKey, $targetClassName, array $validationGroups = [])
266 {
267 $conjunctionValidator = new ConjunctionValidator();
268 $this->baseValidatorConjunctions[$indexKey] = $conjunctionValidator;
269
270 // note: the simpleType check reduces lookups to the class loader
271 if (!TypeHandlingUtility::isSimpleType($targetClassName) && class_exists($targetClassName)) {
272 // Model based validator
273 /** @var \TYPO3\CMS\Extbase\Validation\Validator\GenericObjectValidator $objectValidator */
274 $objectValidator = $this->objectManager->get(\TYPO3\CMS\Extbase\Validation\Validator\GenericObjectValidator::class, []);
275 foreach ($this->reflectionService->getClassPropertyNames($targetClassName) as $classPropertyName) {
276 $classPropertyTagsValues = $this->reflectionService->getPropertyTagsValues($targetClassName, $classPropertyName);
277
278 if (!isset($classPropertyTagsValues['var'])) {
279 throw new \InvalidArgumentException(sprintf('There is no @var annotation for property "%s" in class "%s".', $classPropertyName, $targetClassName), 1363778104);
280 }
281 try {
282 $parsedType = TypeHandlingUtility::parseType(trim(implode('', $classPropertyTagsValues['var']), ' \\'));
283 } catch (\TYPO3\CMS\Extbase\Utility\Exception\InvalidTypeException $exception) {
284 throw new \InvalidArgumentException(sprintf(' @var annotation of ' . $exception->getMessage(), 'class "' . $targetClassName . '", property "' . $classPropertyName . '"'), 1315564744, $exception);
285 }
286 $propertyTargetClassName = $parsedType['type'];
287 // note: the outer simpleType check reduces lookups to the class loader
288 if (!TypeHandlingUtility::isSimpleType($propertyTargetClassName)) {
289 if (TypeHandlingUtility::isCollectionType($propertyTargetClassName)) {
290 $collectionValidator = $this->createValidator(\TYPO3\CMS\Extbase\Validation\Validator\CollectionValidator::class, ['elementType' => $parsedType['elementType'], 'validationGroups' => $validationGroups]);
291 $objectValidator->addPropertyValidator($classPropertyName, $collectionValidator);
292 } elseif (class_exists($propertyTargetClassName) && !TypeHandlingUtility::isCoreType($propertyTargetClassName) && $this->objectManager->isRegistered($propertyTargetClassName) && $this->objectManager->getScope($propertyTargetClassName) === \TYPO3\CMS\Extbase\Object\Container\Container::SCOPE_PROTOTYPE) {
293 $validatorForProperty = $this->getBaseValidatorConjunction($propertyTargetClassName);
294 if ($validatorForProperty !== null && $validatorForProperty->count() > 0) {
295 $objectValidator->addPropertyValidator($classPropertyName, $validatorForProperty);
296 }
297 }
298 }
299
300 $validateAnnotations = [];
301 // @todo: Resolve annotations via reflectionService once its available
302 if (isset($classPropertyTagsValues['validate']) && is_array($classPropertyTagsValues['validate'])) {
303 foreach ($classPropertyTagsValues['validate'] as $validateValue) {
304 $parsedAnnotations = $this->parseValidatorAnnotation($validateValue);
305
306 foreach ($parsedAnnotations['validators'] as $validator) {
307 array_push($validateAnnotations, [
308 'argumentName' => $parsedAnnotations['argumentName'],
309 'validatorName' => $validator['validatorName'],
310 'validatorOptions' => $validator['validatorOptions']
311 ]);
312 }
313 }
314 }
315
316 foreach ($validateAnnotations as $validateAnnotation) {
317 // @todo: Respect validationGroups
318 $newValidator = $this->createValidator($validateAnnotation['validatorName'], $validateAnnotation['validatorOptions']);
319 if ($newValidator === null) {
320 throw new Exception\NoSuchValidatorException('Invalid validate annotation in ' . $targetClassName . '::' . $classPropertyName . ': Could not resolve class name for validator "' . $validateAnnotation->type . '".', 1241098027);
321 }
322 $objectValidator->addPropertyValidator($classPropertyName, $newValidator);
323 }
324 }
325
326 if (!empty($objectValidator->getPropertyValidators())) {
327 $conjunctionValidator->addValidator($objectValidator);
328 }
329 }
330
331 $this->addCustomValidators($targetClassName, $conjunctionValidator);
332 }
333
334 /**
335 * This adds custom validators to the passed $conjunctionValidator.
336 *
337 * A custom validator is found if it follows the naming convention "Replace '\Model\' by '\Validator\' and
338 * append 'Validator'". If found, it will be added to the $conjunctionValidator.
339 *
340 * In addition canValidate() will be called on all implementations of the ObjectValidatorInterface to find
341 * all validators that could validate the target. The one with the highest priority will be added as well.
342 * If multiple validators have the same priority, which one will be added is not deterministic.
343 *
344 * @param string $targetClassName
345 * @param ConjunctionValidator $conjunctionValidator
346 * @return null|Validator\ObjectValidatorInterface
347 */
348 protected function addCustomValidators($targetClassName, ConjunctionValidator &$conjunctionValidator)
349 {
350 // @todo: get rid of ClassNamingUtility usage once we dropped underscored class name support
351 $possibleValidatorClassName = ClassNamingUtility::translateModelNameToValidatorName($targetClassName);
352
353 $customValidator = $this->createValidator($possibleValidatorClassName);
354 if ($customValidator !== null) {
355 $conjunctionValidator->addValidator($customValidator);
356 }
357
358 // @todo: find polytype validator for class
359 }
360
361 /**
362 * Parses the validator options given in @validate annotations.
363 *
364 * @param string $validateValue
365 * @return array
366 */
367 protected function parseValidatorAnnotation($validateValue)
368 {
369 $matches = [];
370 if ($validateValue[0] === '$') {
371 $parts = explode(' ', $validateValue, 2);
372 $validatorConfiguration = ['argumentName' => ltrim($parts[0], '$'), 'validators' => []];
373 preg_match_all(self::PATTERN_MATCH_VALIDATORS, $parts[1], $matches, PREG_SET_ORDER);
374 } else {
375 $validatorConfiguration = ['validators' => []];
376 preg_match_all(self::PATTERN_MATCH_VALIDATORS, $validateValue, $matches, PREG_SET_ORDER);
377 }
378 foreach ($matches as $match) {
379 $validatorOptions = [];
380 if (isset($match['validatorOptions'])) {
381 $validatorOptions = $this->parseValidatorOptions($match['validatorOptions']);
382 }
383 $validatorConfiguration['validators'][] = ['validatorName' => $match['validatorName'], 'validatorOptions' => $validatorOptions];
384 }
385 return $validatorConfiguration;
386 }
387
388 /**
389 * Parses $rawValidatorOptions not containing quoted option values.
390 * $rawValidatorOptions will be an empty string afterwards (pass by ref!).
391 *
392 * @param string $rawValidatorOptions
393 * @return array An array of optionName/optionValue pairs
394 */
395 protected function parseValidatorOptions($rawValidatorOptions)
396 {
397 $validatorOptions = [];
398 $parsedValidatorOptions = [];
399 preg_match_all(self::PATTERN_MATCH_VALIDATOROPTIONS, $rawValidatorOptions, $validatorOptions, PREG_SET_ORDER);
400 foreach ($validatorOptions as $validatorOption) {
401 $parsedValidatorOptions[trim($validatorOption['optionName'])] = trim($validatorOption['optionValue']);
402 }
403 array_walk($parsedValidatorOptions, [$this, 'unquoteString']);
404 return $parsedValidatorOptions;
405 }
406
407 /**
408 * Removes escapings from a given argument string and trims the outermost
409 * quotes.
410 *
411 * This method is meant as a helper for regular expression results.
412 *
413 * @param string &$quotedValue Value to unquote
414 */
415 protected function unquoteString(&$quotedValue)
416 {
417 switch ($quotedValue[0]) {
418 case '"':
419 $quotedValue = str_replace('\\"', '"', trim($quotedValue, '"'));
420 break;
421 case '\'':
422 $quotedValue = str_replace('\\\'', '\'', trim($quotedValue, '\''));
423 break;
424 }
425 $quotedValue = str_replace('\\\\', '\\', $quotedValue);
426 }
427
428 /**
429 * Returns an object of an appropriate validator for the given class. If no validator is available
430 * FALSE is returned
431 *
432 * @param string $validatorName Either the fully qualified class name of the validator or the short name of a built-in validator
433 *
434 * @throws Exception\NoSuchValidatorException
435 * @return string Name of the validator object
436 */
437 protected function resolveValidatorObjectName($validatorName)
438 {
439 if (strpos($validatorName, ':') !== false || strpbrk($validatorName, '_\\') === false) {
440 // Found shorthand validator, either extbase or foreign extension
441 // NotEmpty or Acme.MyPck.Ext:MyValidator
442 list($extensionName, $extensionValidatorName) = explode(':', $validatorName);
443
444 if ($validatorName !== $extensionName && $extensionValidatorName !== '') {
445 // Shorthand custom
446 if (strpos($extensionName, '.') !== false) {
447 $extensionNameParts = explode('.', $extensionName);
448 $extensionName = array_pop($extensionNameParts);
449 $vendorName = implode('\\', $extensionNameParts);
450 $possibleClassName = $vendorName . '\\' . $extensionName . '\\Validation\\Validator\\' . $extensionValidatorName;
451 } else {
452 $possibleClassName = 'Tx_' . $extensionName . '_Validation_Validator_' . $extensionValidatorName;
453 }
454 } else {
455 // Shorthand built in
456 $possibleClassName = 'TYPO3\\CMS\\Extbase\\Validation\\Validator\\' . $this->getValidatorType($validatorName);
457 }
458 } else {
459 // Full qualified
460 // Tx_MyExt_Validation_Validator_MyValidator or \Acme\Ext\Validation\Validator\FooValidator
461 $possibleClassName = $validatorName;
462 if (!empty($possibleClassName) && $possibleClassName[0] === '\\') {
463 $possibleClassName = substr($possibleClassName, 1);
464 }
465 }
466
467 if (substr($possibleClassName, - strlen('Validator')) !== 'Validator') {
468 $possibleClassName .= 'Validator';
469 }
470
471 if (class_exists($possibleClassName)) {
472 $possibleClassNameInterfaces = class_implements($possibleClassName);
473 if (!in_array(\TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface::class, $possibleClassNameInterfaces)) {
474 // The guessed validatorname is a valid class name, but does not implement the ValidatorInterface
475 throw new NoSuchValidatorException('Validator class ' . $validatorName . ' must implement \TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface', 1365776838);
476 }
477 $resolvedValidatorName = $possibleClassName;
478 } else {
479 throw new NoSuchValidatorException('Validator class ' . $validatorName . ' does not exist', 1365799920);
480 }
481
482 return $resolvedValidatorName;
483 }
484
485 /**
486 * Used to map PHP types to validator types.
487 *
488 * @param string $type Data type to unify
489 * @return string unified data type
490 */
491 protected function getValidatorType($type)
492 {
493 switch ($type) {
494 case 'int':
495 $type = 'Integer';
496 break;
497 case 'bool':
498 $type = 'Boolean';
499 break;
500 case 'double':
501 $type = 'Float';
502 break;
503 case 'numeric':
504 $type = 'Number';
505 break;
506 case 'mixed':
507 $type = 'Raw';
508 break;
509 default:
510 $type = ucfirst($type);
511 }
512 return $type;
513 }
514
515 /**
516 * Temporary replacement for $this->reflectionService->getMethodAnnotations()
517 *
518 * @param string $className
519 * @param string $methodName
520 *
521 * @return array
522 */
523 public function getMethodValidateAnnotations($className, $methodName)
524 {
525 $validateAnnotations = [];
526 $methodTagsValues = $this->reflectionService->getMethodTagsValues($className, $methodName);
527 if (isset($methodTagsValues['validate']) && is_array($methodTagsValues['validate'])) {
528 foreach ($methodTagsValues['validate'] as $validateValue) {
529 $parsedAnnotations = $this->parseValidatorAnnotation($validateValue);
530
531 foreach ($parsedAnnotations['validators'] as $validator) {
532 array_push($validateAnnotations, [
533 'argumentName' => $parsedAnnotations['argumentName'],
534 'validatorName' => $validator['validatorName'],
535 'validatorOptions' => $validator['validatorOptions']
536 ]);
537 }
538 }
539 }
540
541 return $validateAnnotations;
542 }
543 }