From 2b2147489e02e110edafc6aa595f0eb7fcfa49d5 Mon Sep 17 00:00:00 2001 From: Steve Boyd Date: Tue, 1 Oct 2024 18:30:48 +1300 Subject: [PATCH] NEW Validate DBFields --- _config/model.yml | 6 ++ composer.json | 1 + src/Core/Injector/InjectionCreator.php | 7 +- src/Forms/EmailField.php | 42 +++--------- src/Forms/FormField.php | 20 +++++- src/Forms/TextField.php | 34 +++------ src/ORM/DataObject.php | 9 +++ src/ORM/FieldType/DBDecimal.php | 10 +++ src/ORM/FieldType/DBEmail.php | 31 +++++++++ src/ORM/FieldType/DBField.php | 21 +++++- src/ORM/FieldType/DBInt.php | 2 + src/ORM/FieldType/DBMediumInt.php | 24 +++++++ src/ORM/FieldType/DBNullableVarchar.php | 12 ++++ src/ORM/FieldType/DBVarchar.php | 8 +++ src/Validation/AbstractSymfonyValidator.php | 36 ++++++++++ src/Validation/BooleanValidator.php | 27 ++++++++ src/Validation/CompositeValidator.php | 26 +++++++ src/Validation/DateValidator.php | 25 +++++++ src/Validation/DatetimeValidator.php | 24 +++++++ src/Validation/DecimalValidator.php | 60 ++++++++++++++++ src/Validation/EmailValidator.php | 19 ++++++ src/Validation/EnumValidator.php | 26 +++++++ src/Validation/FieldValidator.php | 76 +++++++++++++++++++++ src/Validation/IntValidator.php | 42 ++++++++++++ src/Validation/IpValidator.php | 19 ++++++ src/Validation/LocaleValidator.php | 22 ++++++ src/Validation/NumericValidator.php | 19 ++++++ src/Validation/StringValidator.php | 63 +++++++++++++++++ src/Validation/TimeValidator.php | 25 +++++++ src/Validation/UrlValidator.php | 19 ++++++ src/Validation/legacy_DateTimeValidator.php | 18 +++++ src/Validation/legacy_DateValidator.php | 44 ++++++++++++ src/Validation/legacy_TimeValidator.php | 18 +++++ 33 files changed, 771 insertions(+), 64 deletions(-) create mode 100644 src/ORM/FieldType/DBEmail.php create mode 100644 src/ORM/FieldType/DBMediumInt.php create mode 100644 src/ORM/FieldType/DBNullableVarchar.php create mode 100644 src/Validation/AbstractSymfonyValidator.php create mode 100644 src/Validation/BooleanValidator.php create mode 100644 src/Validation/CompositeValidator.php create mode 100644 src/Validation/DateValidator.php create mode 100644 src/Validation/DatetimeValidator.php create mode 100644 src/Validation/DecimalValidator.php create mode 100644 src/Validation/EmailValidator.php create mode 100644 src/Validation/EnumValidator.php create mode 100644 src/Validation/FieldValidator.php create mode 100644 src/Validation/IntValidator.php create mode 100644 src/Validation/IpValidator.php create mode 100644 src/Validation/LocaleValidator.php create mode 100644 src/Validation/NumericValidator.php create mode 100644 src/Validation/StringValidator.php create mode 100644 src/Validation/TimeValidator.php create mode 100644 src/Validation/UrlValidator.php create mode 100644 src/Validation/legacy_DateTimeValidator.php create mode 100644 src/Validation/legacy_DateValidator.php create mode 100644 src/Validation/legacy_TimeValidator.php diff --git a/_config/model.yml b/_config/model.yml index 4046feddb..3d8a565e4 100644 --- a/_config/model.yml +++ b/_config/model.yml @@ -2,6 +2,8 @@ Name: corefieldtypes --- SilverStripe\Core\Injector\Injector: + NullableVarchar: + class: SilverStripe\ORM\FieldType\DBNullableVarchar Boolean: class: SilverStripe\ORM\FieldType\DBBoolean Currency: @@ -20,6 +22,8 @@ SilverStripe\Core\Injector\Injector: class: SilverStripe\ORM\FieldType\DBDecimal Double: class: SilverStripe\ORM\FieldType\DBDouble + Email: + class: SilverStripe\ORM\FieldType\DBEmail Enum: class: SilverStripe\ORM\FieldType\DBEnum Float: @@ -36,6 +40,8 @@ SilverStripe\Core\Injector\Injector: class: SilverStripe\ORM\FieldType\DBHTMLVarchar Int: class: SilverStripe\ORM\FieldType\DBInt + MediumInt: + class: SilverStripe\ORM\FieldType\DBMediumInt BigInt: class: SilverStripe\ORM\FieldType\DBBigInt Locale: diff --git a/composer.json b/composer.json index 8db93b4ad..ddde2b617 100644 --- a/composer.json +++ b/composer.json @@ -47,6 +47,7 @@ "symfony/dom-crawler": "^7.0", "symfony/filesystem": "^7.0", "symfony/http-foundation": "^7.0", + "symfony/intl": "^7.0", "symfony/mailer": "^7.0", "symfony/mime": "^7.0", "symfony/translation": "^7.0", diff --git a/src/Core/Injector/InjectionCreator.php b/src/Core/Injector/InjectionCreator.php index 6b1ae121a..caebdda3f 100644 --- a/src/Core/Injector/InjectionCreator.php +++ b/src/Core/Injector/InjectionCreator.php @@ -2,6 +2,7 @@ namespace SilverStripe\Core\Injector; +use Exception; use InvalidArgumentException; /** @@ -23,6 +24,10 @@ class InjectionCreator implements Factory // Ensure there are no string keys as they cannot be unpacked with the `...` operator $values = array_values($params); - return new $class(...$values); + try { + return new $class(...$values); + } catch (Exception $e) { + throw $e; + } } } diff --git a/src/Forms/EmailField.php b/src/Forms/EmailField.php index 68ea66d41..43879568f 100644 --- a/src/Forms/EmailField.php +++ b/src/Forms/EmailField.php @@ -2,11 +2,18 @@ namespace SilverStripe\Forms; +use SilverStripe\Validation\EmailValidator; + /** - * Text input field with validation for correct email format according to RFC 2822. + * Text input field with validation for correct email format */ class EmailField extends TextField { + private static array $field_validators = [ + [ + 'class' => EmailValidator::class, + ], + ]; protected $inputType = 'email'; /** @@ -17,39 +24,6 @@ class EmailField extends TextField return 'email text'; } - /** - * Validates for RFC 2822 compliant email addresses. - * - * @see http://www.regular-expressions.info/email.html - * @see http://www.ietf.org/rfc/rfc2822.txt - * - * @param Validator $validator - * - * @return string - */ - public function validate($validator) - { - $result = true; - $this->value = trim($this->value ?? ''); - - $pattern = '^[a-z0-9!#$%&\'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&\'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$'; - - // Escape delimiter characters. - $safePattern = str_replace('/', '\\/', $pattern ?? ''); - - if ($this->value && !preg_match('/' . $safePattern . '/i', $this->value ?? '')) { - $validator->validationError( - $this->name, - _t('SilverStripe\\Forms\\EmailField.VALIDATION', 'Please enter an email address'), - 'validation' - ); - - $result = false; - } - - return $this->extendValidationResult($result, $validator); - } - public function getSchemaValidation() { $rules = parent::getSchemaValidation(); diff --git a/src/Forms/FormField.php b/src/Forms/FormField.php index 0d210436b..8b14e1bd7 100644 --- a/src/Forms/FormField.php +++ b/src/Forms/FormField.php @@ -15,6 +15,8 @@ use SilverStripe\Core\Validation\ValidationResult; use SilverStripe\View\AttributesHTML; use SilverStripe\View\SSViewer; use SilverStripe\Model\ModelData; +use SilverStripe\Core\Injector\Injector; +use SilverStripe\Validation\FieldValidator; /** * Represents a field in a form. @@ -275,6 +277,8 @@ class FormField extends RequestHandler 'Description' => 'HTMLFragment', ]; + private static array $field_validators = []; + /** * Structured schema state representing the FormField's current data and validation. * Used to render the FormField as a ReactJS Component on the front-end. @@ -1231,15 +1235,25 @@ class FormField extends RequestHandler } /** - * Abstract method each {@link FormField} subclass must implement, determines whether the field - * is valid or not based on the value. + * Subclasses can define an existing FieldValidatorClass to validate the FormField value + * They may also override this method to provide custom validation logic * * @param Validator $validator * @return bool */ public function validate($validator) { - return $this->extendValidationResult(true, $validator); + $isValid = true; + $name = strip_tags($this->Title() ? $this->Title() : $this->getName()); + $fieldValidators = FieldValidator::createFieldValidatorsForField($this, $name, $this->value); + foreach ($fieldValidators as $fieldValidator) { + $validationResult = $fieldValidator->validate(); + if (!$validationResult->isValid()) { + $validator->getResult()->combineAnd($validationResult); + $isValid = false; + } + } + return $this->extendValidationResult($isValid, $validator); } /** diff --git a/src/Forms/TextField.php b/src/Forms/TextField.php index 49aa40b2c..b994a39cc 100644 --- a/src/Forms/TextField.php +++ b/src/Forms/TextField.php @@ -2,6 +2,8 @@ namespace SilverStripe\Forms; +use SilverStripe\Validation\StringValidator; + /** * Text input field. */ @@ -14,6 +16,13 @@ class TextField extends FormField implements TippableFieldInterface protected $schemaDataType = FormField::SCHEMA_DATA_TYPE_TEXT; + private static array $field_validators = [ + [ + 'class' => StringValidator::class, + 'argCalls' => [null, 'getMaxLength'], + ] + ]; + /** * @var Tip|null A tip to render beside the input */ @@ -117,31 +126,6 @@ class TextField extends FormField implements TippableFieldInterface return $data; } - /** - * Validate this field - * - * @param Validator $validator - * @return bool - */ - public function validate($validator) - { - $result = true; - if (!is_null($this->maxLength) && mb_strlen($this->value ?? '') > $this->maxLength) { - $name = strip_tags($this->Title() ? $this->Title() : $this->getName()); - $validator->validationError( - $this->name, - _t( - 'SilverStripe\\Forms\\TextField.VALIDATEMAXLENGTH', - 'The value for {name} must not exceed {maxLength} characters in length', - ['name' => $name, 'maxLength' => $this->maxLength] - ), - "validation" - ); - $result = false; - } - return $this->extendValidationResult($result, $validator); - } - public function getSchemaValidation() { $rules = parent::getSchemaValidation(); diff --git a/src/ORM/DataObject.php b/src/ORM/DataObject.php index 2b6bed1da..8968d59c3 100644 --- a/src/ORM/DataObject.php +++ b/src/ORM/DataObject.php @@ -1230,6 +1230,15 @@ class DataObject extends ModelData implements DataObjectInterface, i18nEntityPro public function validate() { $result = ValidationResult::create(); + // Call DBField::validate() on every DBField + $specs = static::getSchema()->fieldSpecs(static::class); + foreach (array_keys($specs) as $fieldName) { + $dbField = $this->dbObject($fieldName); + $validationResult = $dbField->validate(); + if (!$validationResult->isValid()) { + $result->combineAnd($validationResult); + } + } $this->extend('updateValidate', $result); return $result; } diff --git a/src/ORM/FieldType/DBDecimal.php b/src/ORM/FieldType/DBDecimal.php index deab1bcd0..c6c8e44d4 100644 --- a/src/ORM/FieldType/DBDecimal.php +++ b/src/ORM/FieldType/DBDecimal.php @@ -50,6 +50,16 @@ class DBDecimal extends DBField return floor($this->value ?? 0.0); } + public function getWholeSize(): int + { + return $this->wholeSize; + } + + public function getDecimalSize(): int + { + return $this->decimalSize; + } + public function requireField(): void { $parts = [ diff --git a/src/ORM/FieldType/DBEmail.php b/src/ORM/FieldType/DBEmail.php new file mode 100644 index 000000000..fb1e839dd --- /dev/null +++ b/src/ORM/FieldType/DBEmail.php @@ -0,0 +1,31 @@ + EmailValidator::class, + ], + ]; + + public function scaffoldFormField(?string $title = null, array $params = []): ?FormField + { + // Set field with appropriate size + $field = EmailField::create($this->name, $title); + $field->setMaxLength($this->getSize()); + + // Allow the user to select if it's null instead of automatically assuming empty string is + if (!$this->getNullifyEmpty()) { + return NullableField::create($field); + } + return $field; + } +} diff --git a/src/ORM/FieldType/DBField.php b/src/ORM/FieldType/DBField.php index 38efb5758..1e5869b24 100644 --- a/src/ORM/FieldType/DBField.php +++ b/src/ORM/FieldType/DBField.php @@ -10,6 +10,8 @@ use SilverStripe\Forms\TextField; use SilverStripe\ORM\Filters\SearchFilter; use SilverStripe\ORM\Queries\SQLSelect; use SilverStripe\Model\ModelData; +use SilverStripe\Core\Validation\ValidationResult; +use SilverStripe\Validation\FieldValidator; /** * Single field in the database. @@ -43,7 +45,6 @@ use SilverStripe\Model\ModelData; */ abstract class DBField extends ModelData implements DBIndexable { - /** * Raw value of this field */ @@ -99,6 +100,8 @@ abstract class DBField extends ModelData implements DBIndexable 'ProcessedRAW' => 'HTMLFragment', ]; + private static array $field_validators = []; + /** * Default value in the database. * Might be overridden on DataObject-level, but still useful for setting defaults on @@ -468,6 +471,22 @@ abstract class DBField extends ModelData implements DBIndexable } } + /** + * Validate this field. Called during DataObject::validate(). + */ + public function validate(): ValidationResult + { + $result = ValidationResult::create(); + $fieldValidators = FieldValidator::createFieldValidatorsForField($this, $this->getName(), $this->getValue()); + foreach ($fieldValidators as $fieldValidator) { + $validationResult = $fieldValidator->validate(); + if (!$validationResult->isValid()) { + $result->combineAnd($validationResult); + } + } + return $result; + } + /** * Returns a FormField instance used as a default * for form scaffolding. diff --git a/src/ORM/FieldType/DBInt.php b/src/ORM/FieldType/DBInt.php index 0f46ddf54..9d01b92ff 100644 --- a/src/ORM/FieldType/DBInt.php +++ b/src/ORM/FieldType/DBInt.php @@ -11,6 +11,8 @@ use SilverStripe\Model\ArrayData; /** * Represents a signed 32 bit integer field. + * + * Ints are always signed i.e. they can be negative */ class DBInt extends DBField { diff --git a/src/ORM/FieldType/DBMediumInt.php b/src/ORM/FieldType/DBMediumInt.php new file mode 100644 index 000000000..995455b57 --- /dev/null +++ b/src/ORM/FieldType/DBMediumInt.php @@ -0,0 +1,24 @@ + 'int', + 'precision' => 8, + 'null' => 'not null', + 'default' => $this->defaultVal, + 'arrayValue' => $this->arrayValue + ]; + $values = ['type' => 'int', 'parts' => $parts]; + DB::require_field($this->tableName, $this->name, $values); + } +} diff --git a/src/ORM/FieldType/DBNullableVarchar.php b/src/ORM/FieldType/DBNullableVarchar.php new file mode 100644 index 000000000..106201488 --- /dev/null +++ b/src/ORM/FieldType/DBNullableVarchar.php @@ -0,0 +1,12 @@ + StringValidator::class, + 'argCalls' => [null, 'getSize'], + ] + ]; + private static array $casting = [ 'Initial' => 'Text', 'URL' => 'Text', diff --git a/src/Validation/AbstractSymfonyValidator.php b/src/Validation/AbstractSymfonyValidator.php new file mode 100644 index 000000000..8ea25ff36 --- /dev/null +++ b/src/Validation/AbstractSymfonyValidator.php @@ -0,0 +1,36 @@ +isValid()) { + return $result; + } + $message = _t(__CLASS__ . '.INVALID', 'Invalid email address'); + $constraintClass = $this->getConstraintClass(); + $constraint = new $constraintClass(message: $message); + $validationResult = ConstraintValidator::validate($this->value, $constraint, $this->name); + return $result->combineAnd($validationResult); + } + + /** + * The symfony constraint class to use + */ + abstract protected function getConstraintClass(): string; + + /** + * The message to use when the value is invalid + */ + abstract protected function getMessage(): string; +} diff --git a/src/Validation/BooleanValidator.php b/src/Validation/BooleanValidator.php new file mode 100644 index 000000000..1a272c155 --- /dev/null +++ b/src/Validation/BooleanValidator.php @@ -0,0 +1,27 @@ +value, self::VALID_VALUES, true)) { + $message = _t(__CLASS__ . '.INVALID', 'Invalid value'); + $result->addFieldError($this->name, $message); + } + return $result; + } +} diff --git a/src/Validation/CompositeValidator.php b/src/Validation/CompositeValidator.php new file mode 100644 index 000000000..35e709204 --- /dev/null +++ b/src/Validation/CompositeValidator.php @@ -0,0 +1,26 @@ +children = $children; + } + + protected function validateValue(ValidationResult $result): ValidationResult + { + foreach ($this->children as $child) { + $result->combineAnd($child->validate()); + } + return $result; + } +} diff --git a/src/Validation/DateValidator.php b/src/Validation/DateValidator.php new file mode 100644 index 000000000..f55ecea9e --- /dev/null +++ b/src/Validation/DateValidator.php @@ -0,0 +1,25 @@ +wholeSize = $wholeSize; + $this->decimalSize = $decimalSize; + } + + protected function validateValue(ValidationResult $result): ValidationResult + { + $result = parent::validateValue($result); + if (!$result->isValid()) { + return $result; + } + // Example of how digits are stored in the database + // Decimal(9,2) is allowed a total of 9 digits, and will always round to 2 decimal places + // This means it has a maximum 7 digits before the decimal point + // + // Valid + // 1234567.99 + // 9999999.99 + // -9999999.99 + // 1234567.999 - will round to 1234568.00 + // + // Not valid + // 12345678.9 - 8 digits the before the decimal point + // 1234567.891 - 10 digits total + // 9999999.999 - would be rounted to 10000000.00 which exceeds the 9 digits + + // Convert to absolute value - any the minus sign is not counted + $absValue = abs($this->value); + // Round to the decimal size which is what the database will do + $rounded = round($absValue, $this->decimalSize); + // Count this number of digits - note the minus 1 for the decimal point + $digitCount = strlen((string) $rounded) - 1; + if ($digitCount > $this->wholeSize) { + $message = _t(__CLASS__ . '.TOOLARGE', 'Number is too large'); + $result->addFieldError($this->name, $message); + } + return $result; + } +} diff --git a/src/Validation/EmailValidator.php b/src/Validation/EmailValidator.php new file mode 100644 index 000000000..6221e40c9 --- /dev/null +++ b/src/Validation/EmailValidator.php @@ -0,0 +1,19 @@ +allowedValues = $allowedValues; + } + + protected function validateValue(ValidationResult $result): ValidationResult + { + if (!in_array($this->value, $this->allowedValues)) { + $message = _t(__CLASS__ . '.NOTALLOWED', 'Not an allowed value'); + $result->addFieldError($this->name, $message); + } + return $result; + } +} diff --git a/src/Validation/FieldValidator.php b/src/Validation/FieldValidator.php new file mode 100644 index 000000000..dbd8d0eba --- /dev/null +++ b/src/Validation/FieldValidator.php @@ -0,0 +1,76 @@ +name = $name; + $this->value = $value; + } + + /** + * Validate the value + */ + public function validate(): ValidationResult + { + $result = ValidationResult::create(); + $result = $this->validateValue($result); + return $result; + } + + /** + * Inner validatation method that that is implemented by subclasses + */ + abstract protected function validateValue(ValidationResult $result): ValidationResult; + + /** + * Create FieldValidators for a field based on the field's configuration + */ + public static function createFieldValidatorsForField( + FormField|DBField $field, + string $name, + mixed $value + ): array { + $fieldValidators = []; + $config = $field->config()->get('field_validators'); + foreach ($config as $spec) { + $class = $spec['class']; + $argCalls = $spec['argCalls'] ?? null; + if (!is_a($class, FieldValidator::class, true)) { + throw new RuntimeException("Class $class is not a FieldValidator"); + } + $args = [$name, $value]; + if (!is_null($argCalls)) { + if (!is_array($argCalls)) { + throw new RuntimeException("argCalls for $class is not an array"); + } + foreach ($argCalls as $i => $argCall) { + if (!is_string($argCall) && !is_null($argCall)) { + throw new RuntimeException("argCall $i for $class is not a string or null"); + } + if ($argCall) { + $args[] = call_user_func([$field, $argCall]); + } else { + $args[] = null; + } + } + } + $fieldValidators[] = Injector::inst()->createWithArgs($class, $args); + } + return $fieldValidators; + } +} diff --git a/src/Validation/IntValidator.php b/src/Validation/IntValidator.php new file mode 100644 index 000000000..aab59fbcd --- /dev/null +++ b/src/Validation/IntValidator.php @@ -0,0 +1,42 @@ +minValue = $minValue; + $this->maxValue = $maxValue; + } + + protected function validateValue(ValidationResult $result): ValidationResult + { + $result = parent::validateValue($result); + if (!$result->isValid()) { + return $result; + } + if ($this->value < $this->minValue) { + $message = _t(__CLASS__ . '.TOOSMALL', 'Value is too small'); + $result->addFieldError($this->name, $message); + } elseif ($this->value > $this->maxValue) { + $message = _t(__CLASS__ . '.TOOLARGE', 'Value is too large'); + $result->addFieldError($this->name, $message); + } + return $result; + } +} diff --git a/src/Validation/IpValidator.php b/src/Validation/IpValidator.php new file mode 100644 index 000000000..37a7c535a --- /dev/null +++ b/src/Validation/IpValidator.php @@ -0,0 +1,19 @@ +value)) { + $message = _t(__CLASS__ . '.NOTNUMERIC', 'Must be a number'); + $result->addFieldError($this->name, $message); + return $result; + } + return $result; + } +} diff --git a/src/Validation/StringValidator.php b/src/Validation/StringValidator.php new file mode 100644 index 000000000..7c1312ecb --- /dev/null +++ b/src/Validation/StringValidator.php @@ -0,0 +1,63 @@ +minLength = $minLength; + $this->maxLength = $maxLength; + } + + protected function validateValue(ValidationResult $result): ValidationResult + { + // Allow blank values + if (!$this->value) { + return $result; + } + if (!is_string($this->value)) { + $message = _t( + __CLASS__ . '.INVALID', + '{name} must be a string', + ['name' => $this->name] + ); + $result->addFieldError($this->name, $message); + } + $len = mb_strlen($this->value); + if (!is_null($this->minLength) && $len < $this->minLength) { + $message = _t( + __CLASS__ . '.TOOSHORT', + '{name} must have at least {minLength} characters', + ['name' => $this->name, 'minLength' => $this->minLength] + ); + $result->addFieldError($this->name, $message); + } + if (!is_null($this->maxLength) && $len > $this->maxLength) { + $message = _t( + __CLASS__ . '.TOOLONG', + '{name} cannot have more than {maxLength} characters', + ['name' => $this->name, 'maxLength' => $this->maxLength] + ); + $result->addFieldError($this->name, $message); + } + return $result; + } +} diff --git a/src/Validation/TimeValidator.php b/src/Validation/TimeValidator.php new file mode 100644 index 000000000..83b5d6c79 --- /dev/null +++ b/src/Validation/TimeValidator.php @@ -0,0 +1,25 @@ +value) { + return $result; + } + // Attempt to parse the date. If this fails, the date is invalid. + $date = date_create_from_format($this->getFormat(), $this->value); + if ($date === false) { + $result->addFieldError($this->name, $this->getMessage()); + } + return $result; + } + + /** + * Get the PHP date format to use for date parsing + */ + protected function getFormat() + { + // This validator uses PHP date format codes + // The IntlDateFormatter used by DBDate uses ISO date format codes instead of PHP date format codes + // For ISO format codes see + // http://framework.zend.com/manual/1.12/en/zend.date.constants.html#zend.date.constants.selfdefinedformats + // For PHP format codes see https://www.php.net/manual/en/datetime.format.php + return 'Y-m-d'; + } + + /** + * Get the error message to use when validation fails + */ + protected function getMessage() + { + return _t(__CLASS__ . '.INVALID', 'Invalid date'); + } +} diff --git a/src/Validation/legacy_TimeValidator.php b/src/Validation/legacy_TimeValidator.php new file mode 100644 index 000000000..5db8ec4e7 --- /dev/null +++ b/src/Validation/legacy_TimeValidator.php @@ -0,0 +1,18 @@ +