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