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