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