Laravel Validation: only allow known properties/attributes, otherwise fail validation - php

We are building an api endpoint where precision is required. We want to enforce strict validation on the parameters that are POST/PUT to the server.
If the api user sends a key=value pair that is not supported (eg. we allow the parameters [first_name, last_name] and the user includes an unsupported parameter [country]), we want the validation to fail.
Have tried building a custom validator called allowed_attributes (used as allowed_attributes:attr1,attr2,...), but for it to be usable in a $validationRules array, it has to be applied to the parent of a list of nested/child attributes (...because otherwise our custom validator did not have access to the attributes being validated).
Validator::extend('allowed_attributes', 'App\Validators\AllowedAttributesValidator#validate');
This created issues with other validators, where we then had to anticipate this parent/child structure and code around it, including additional post-validation clean-up of error keys and error message strings.
tl;dr: very dirty, not a clean implementation.
$validationRules = [
'parent' => 'allowed_attributes:first_name,last_name',
'parent.first_name' => 'required|string|max:40',
'parent.last_name' => 'required|string|max:40'
];
$isValid = Validator::make(['parent' => $request], $validationRules);
var_dump("Validation results: " . ($isValid ? "passed" : "failed"));
Any ideas/suggestions on how this can be accomplished more cleanly in laravel, without requiring the use of parent/child relationship to get access to the list of all $request attributes (within the custom validator)?

I preferred to post a new answer as the approach is different from the previous one and a bit more cleaner. So I would rather keep the two approaches separated and not mixed together in the same answer.
Better problem handling
After digging deeper into the Validation's namespace's source code since my last answer I figured out that the easiest way would have been to extend the Validator class to remplement the passes() function to also check what you needed.
This implementation has the benefit to also correcly handle specific error messages for single array/object fields without any effor and should be fully compatible with the usual error messages translations.
Create a custom validator class
You should first create a Validator class within your app folder (I placed it under app/Validation/Validator.php) and implement the passes method like this:
<?php
namespace App\Validation;
use Illuminate\Support\Arr;
use Illuminate\Validation\Validator as BaseValidator;
class Validator extends BaseValidator
{
/**
* Determine if the data passes the validation rules.
*
* #return bool
*/
public function passes()
{
// Perform the usual rules validation, but at this step ignore the
// return value as we still have to validate the allowance of the fields
// The error messages count will be recalculated later and returned.
parent::passes();
// Compute the difference between the request data as a dot notation
// array and the attributes which have a rule in the current validator instance
$extraAttributes = array_diff_key(
Arr::dot($this->data),
$this->rules
);
// We'll spin through each key that hasn't been stripped in the
// previous filtering. Most likely the fields will be top level
// forbidden values or array/object values, as they get mapped with
// indexes other than asterisks (the key will differ from the rule
// and won't match at earlier stage).
// We have to do a deeper check if a rule with that array/object
// structure has been specified.
foreach ($extraAttributes as $attribute => $value) {
if (empty($this->getExplicitKeys($attribute))) {
$this->addFailure($attribute, 'forbidden_attribute', ['value' => $value]);
}
}
return $this->messages->isEmpty();
}
}
This would essentially extend the default Validator class to add additional checks on the passes method. The check compute the array difference by keys between the input attributes converted to dot notation (to support array/object validation) and the attributes which have at least one rule assigned.
Replace the default Validator in the container
Then the last step you miss is to bind the new Validator class in the boot method of a service provider. To do so you can just override the resolver of the Illuminate\Validation\Factory class binded into the IoC container as 'validator':
// Do not forget the class import at the top of the file!
use App\Validation\Validator;
// ...
/**
* Bootstrap any application services.
*
* #return void
*/
public function boot()
{
$this->app->make('validator')
->resolver(function ($translator, $data, $rules, $messages, $attributes) {
return new Validator($translator, $data, $rules, $messages, $attributes);
});
}
// ...
Pratical use in a controller
You don't have to do anything specific to use this feature. Just call the validate method as usual:
$this->validate(request(), [
'first_name' => 'required|string|max:40',
'last_name' => 'required|string|max:40'
]);
Customize Error messages
To customize the error message you just have to add a translation key in your lang file with a key equal to forbidden_attribute (you can customize the error key name in the custom Validator class on the addFailure method call).
Example: resources/lang/en/validation.php
<?php
return [
// ...
'forbidden_attribute' => 'The :attribute key is not allowed in the request body.',
// ...
];
Note: this implementation has been tested in Laravel 5.3 only.

It should work for simple key/value pairs with this custom validator:
Validator::extendImplicit('allowed_attributes', function ($attribute, $value, $parameters, $validator) {
// If the attribute to validate request top level
if (strpos($attribute, '.') === false) {
return in_array($attribute, $parameters);
}
// If the attribute under validation is an array
if (is_array($value)) {
return empty(array_diff_key($value, array_flip($parameters)));
}
// If the attribute under validation is an object
foreach ($parameters as $parameter) {
if (substr_compare($attribute, $parameter, -strlen($parameter)) === 0) {
return true;
}
}
return false;
});
The validator logic is pretty simple:
If $attribute doesn't contains a ., we're dealing with a top level parameter, and we just have to check if it is present in the allowed_attributes list that we pass to the rule.
If $attribute's value is an array, we diff the input keys with the allowed_attributes list, and check if any attribute key has left. If so, our request had an extra key we didn't expect, so we return false.
Otherwise $attribute's value is an object we have to check if each parameter we're expecting (again, the allowed_attributes list) is the last segment of the current attribute (as laravel gives us the full dot notated attribute in $attribute).
The key here is to apply it to validation rules should like this (note the first validation rule):
$validationRules = [
'parent.*' => 'allowed_attributes:first_name,last_name',
'parent.first_name' => 'required|string|max:40',
'parent.last_name' => 'required|string|max:40'
];
The parent.* rule will apply the custom validator to each key of the 'parent' object.
To answer your question
Just don't wrap your request in an object, but use the same concept as above and apply the allowed_attributes rule with a *:
$validationRules = [
'*' => 'allowed_attributes:first_name,last_name',
'first_name' => 'required|string|max:40',
'last_name' => 'required|string|max:40'
];
This will apply the rule to all the present top level input request fields.
NOTE: Keep in mind that laravel validation is influenced by order of the rules as they are putted in rules array.
For example, moving the parent.* rule on bottom will trigger that rule on parent.first_name and parent.last_name; as opposed, keeping it as the first rule will not trigger the validation for the first_name and last_name.
This means that you could eventually remove the attributes that has further validation logic from the allowed_attributes rule's parameter list.
For example, if you would like to require only the first_name and last_name and prohibit any other field in the parent object, you might use these rules:
$validationRules = [
// This will be triggered for all the request fields except first_name and last_name
'parent.*' => 'allowed_attributes',
'parent.first_name' => 'required|string|max:40',
'parent.last_name' => 'required|string|max:40'
];
But, the following WON'T work as expected:
$validationRules = [
'parent.first_name' => 'required|string|max:40',
'parent.last_name' => 'required|string|max:40',
// This, instead would be triggered on all fields, also on first_name and last_name
// If you put this rule as last, you MUST specify the allowed fields.
'parent.*' => 'allowed_attributes',
];
Array Minor Issues
As far as I know, per Laravel's validation logic, if you were up to validate an array of objects, this custom validator would work, but the error message you would get would be generic on the array item, not on the key of that array item that wasn't allowed.
For example, you allow a products field in your request, each with an id:
$validationRules = [
'products.*' => 'allowed_attributes:id',
];
If you validate a request like this:
{
"products": [{
"id": 3
}, {
"id": 17,
"price": 3.49
}]
}
You will get an error on product 2, but you won't be able to tell which field is causing the problem!

Related

Why is my field validated even if it's not required?

I have a request that takes several input. I want the 'salary' input to be validated only if the salary_type is equal to "exact". Else, I don't want to have any message about it.
However, now, even if salary_type is equal to "range", I still get an error that "salary must be an integer".
How can I make sure there isn't any error unless the field is required?
Thanks!
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreJobOfferRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
'title' => 'required|string|min:3',
'description' => 'required|string|min:10',
'city' => 'required|string|min:3',
'salary_type' => 'required|in:range,exact',
'min_salary' => 'required_if:salary_type,range|integer|min:0',
'max_salary' => 'required_if:salary_type,range|integer|min:0',
'salary' => 'required_if:salary_type,exact|integer|min:0',
'fieldsOfWork' => 'required|exists:field_of_works,id',
'job_type.*' => 'required|exists:job_types,id',
"trainings.*" => 'required|exists:trainings,id',
"availability.*" => 'required|exists:trainings,id'
];
}
}
required and required_if do not stop the rest of the validation rules from being executed. It just expects the value not to be null, an empty string, and empty Countable object or an uploaded file with no path. If the field is set, the required rule will just be skipped and the next rule is applied.
So, the required_if rule for salary will skip if salary_type is not 'exact', and continue validating if it's an integer and min 0. This behavior is confusing to a lot of new Laravel developers.
There is an exclude_if rule that does what you probably expect:
'salary' => 'exclude_if:salary_type,exact|integer|min:0',
This will exclude the whole field under validation from the request data that will be returned by validate() and $request->validated() methods if the condition is not met. So if salary_type is not 'exact', the salary field will be gone but the validation will pass.
If salary_type is 'exact', however, it will be validated for being a min 0 integer value.
Please note that the exclude_if rule was added in Laravel 6, so this doesn't work for previous versions of Laravel. See https://laravel.com/docs/8.x/validation#conditionally-adding-rules for details on conditional validation.

cakephp3 custom validation

I have a duration field that sometimes can be empty and sometimes can't, depending on the other data sent by the form. So I'm trying to do custom validation in CakePHP3.
In my table I did
public function validationDefault(Validator $validator)
{
$validator
->add('duration', 'durationOk', [
'rule' => 'isDurationOk',
'message' => 'duration is not OK',
'provider' => 'table'
]);
return $validator;
}
public function isDurationOk($value, $context)
{
// do some logic
return false; // Always return false, just for test
}
Now when I set the value for duration field I get an 'duration is not OK' error (as expected). But when I let the value empty I get a 'This field cannot be left empty' error.
So I added:
->allowEmpty('duration');
But in this case when duration is empty I don't get an error at all.
Am I doing something wrong or it's just me don't understanding how validation works?
Let me read the book for you:
Conditional Validation
When defining validation rules, you can use the on key to define when
a validation rule should be applied. If left undefined, the rule will
always be applied. Other valid values are create and update. Using one
of these values will make the rule apply to only create or update
operations.
Additionally, you can provide a callable function that will determine
whether or not a particular rule should be applied:
'on' => function ($context) {
// Do your "other data" checks here
return !empty($context['data']['other_data']);
}
So just define the conditions depending on your "other data" in the callback to apply the rule only when the conditons are true.
Alternatively you can manipulate the plain form data even before it gets validated in the beforeMarshal() callback of the table and change the form data as needed or load another validator or modify the validator.

Validator - return all error messages

Currently the Validator in Laravel only appears to return one error message per field, despite the field potentially having multiple rules and messages. (Note: I'm currently passing an empty array as $data to Validator::make)
What I'm trying to do is build an array of each field's rules and messages that could potentially be re-used for front end validation. Something like this:
{
"name": {
"required": [
"The name field is required."
],
"max:255": [
"The name field may not be greater than 255."
]
},
"email": {
"required": [
"The email field is required."
],
"email": [
"The email field must be a valid email address."
],
"max:255": [
"The email field may not be greater than 255."
]
}
}
The getMessage method in Illuminate\Validation\Validator looks like it would get me close to being able to construct something myself, however it is a protected method.
Does anyone know of a way to get a Validator instance to output all rules and messages?
Currently the Validator in Laravel only appears to return one error message per field, despite the field potentially having multiple rules and messages.
Validation of given field stops as soon as a single validation rule fails. That's the reason you're getting only single error message per field.
As of fetching the validation messages like in the example you provided, Laravel's validator does not provide such option, but you could easily achieve that by extending the Validator class.
First, create your new class:
<?php namespace Your\Namespace;
use Illuminate\Validation\Validator as BaseValidator;
class Validator extends BaseValidator {
public function getValidationMessages() {
$messages = [];
foreach ($this->rules as $attribute => $rules) {
foreach ($rules as $rule) {
$messages[$attribute][$rule] = $this->getMessage($attribute, $rule);
}
}
return $messages;
}
}
As you can see the output is a bit different than your example. There is no need to return an array of messages for given attribute and rule, as there will be always only one message in the array, so I'm just storing a string there.
Second, you need to make sure that your validator class is used. In order to achieve that, you'll need to register your own validator resolver with Validator facade:
Validator::resolver(function($translator, array $data, array $rules, array $messages, array $customAttributes) {
return new \Your\Namespace\Validator($translator, $data, $rules, $messages, $customAttributes);
});
You can do this in your AppServiceProvider::boot() method.
Now, in order to get validation messages for given validator, you just need to call:
Validator::make($data, $rules)->getValidationMessages();
Keep in mind this code hasn't been tested. Let me know if you see any issues or typos with the code and I'll be more than happy to get that working for you.

Clearing Validation Error Messages from a Zend Form Element

I have a form element for capturing email addresses. I am using Zend_Validate_EmailAddress on the element, and it generates error messages that aren't very user-friendly.
My first step was to specify new messages that were more user-friendly, but some of the checks simply don't lend themselves to a user-friendly message. I then tried to simply clear those messages after running isValid() on the form and specify my own, but none of the functions I've found will clear the messages.
What I've tried and results
setErrorMessages() - Values set here seem to be ignored altogether
clearErrorMessages() - Seems to be ignored
setErrors() - Adds my message, but leaves the others intact
This is the code that displays the errors in my custom view script:
<?php if ($this->element->hasErrors()): ?>
<?php echo $this->formErrors($this->element->getMessages()); ?>
<?php endif; ?>
MY SOLUTION
I'm awarding Gordon with the answer, because his solution is most complete, but I ended up using the addErrorMessage() function on the element like this:
$element->addValidator('EmailAddress', false, $this->_validate['EmailAddress'])
->addErrorMessage("'%value%' is not a valid email address");
$element->addValidator('Date', false, array('MM/dd/yyyy'))
->addErrorMessage("Date must be in MM/DD/YYYY format");
From the Reference Guide (emphasis mine):
Some developers may wish to provide custom error messages for a validator. The $options argument of the Zend_Form_Element::addValidator() method allows you to do so by providing the key 'messages' and mapping it to an array of key/value pairs for setting the message templates. You will need to know the error codes of the various validation error types for the particular validator.
So you can do:
$form = new Zend_Form;
$username = new Zend_Form_Element_Text('username');
$username->addValidator('regex', false, array(
'/^[a-z]/i',
'messages' => array(
'regexInvalid' => 'foo',
'regexNotMatch' => 'bar',
'regexErrorous' => 'baz'
)
));
$form->addElement($username);
$form->isValid(array('username' => '!foo'));
which will then render 'bar' for the error message, because the regex does not match because it doesnt start with a letter from a-Z.
This is equivalent to using:
$username->setErrorMessages(
array(
'regexNotMatch' => 'The value %value% must start with a-Z',
…
)
);
I've used a different pattern to illustrate how to use the validated value in the pattern.
You can also use setErrors, if you want to delete any default templates, e.g.
$username->setErrors(array('The value must start with a-Z'));
Whatever you do, you have to configure that before validating with isValid. Once the validation is run, the Zend_Form_Element will contain the default error message otherwise. I am not aware of any way to reset that then (though someone might want to correct me on that).
Further quoting the reference guide:
A better option is to use a Zend_Translate_Adapter with your form. Error codes are automatically passed to the adapter by the default Errors decorator; you can then specify your own error message strings by setting up translations for the various error codes of your validators.
All the validation messages can be customized from the file in
http://framework.zend.com/svn/framework/standard/trunk/resources/languages/en
The file should be in APPLICATION_PATH/resources/languages, but can really be placed anywhere as long as you tell Zend_Translate where to find it.
When you define a form element like this
$titel = new Zend_Form_Element_Text ( "titel" );
$titel->setLabel ( "Titel" )->setRequired ( true )
->addValidator ( 'regex', false, array ("/[\pL\pN_\-]+/" ) );
you can specify a error message in your view script
<?php
$form = $this->form;
$errorsMessages =$this->form->getMessages();
?>
<div>
<label>Titel</label> <?php echo $form->titel->renderViewHelper()?>
<?php
if(isset($errorsMessages['titel'])){
echo "<p class='error'>There's something wrong!</p>";
}
?>
</div>
I don't know if this conforms your way but I really like defining my forms this way ;)
One way you can attack it is to create your own custom validator by extending the validator you plan on using and overriding the messages. For instance, looking at Zend_Validate_Alnum, it looks like this:
class Zend_Validate_Alnum extends Zend_Validate_Abstract
{
const INVALID = 'alnumInvalid';
const NOT_ALNUM = 'notAlnum';
const STRING_EMPTY = 'alnumStringEmpty';
[ ... ]
protected $_messageTemplates = array(
self::INVALID => "Invalid type given. String, integer or float expected",
self::NOT_ALNUM => "'%value%' contains characters which are non alphabetic and no digits",
self::STRING_EMPTY => "'%value%' is an empty string",
);
[ ... ]
}
Override the $_messageTemplates array in your own class like this
class My_Validate_Alnum extends Zend_Validate_Alnum
{
protected $_messageTemplates = array(
self::INVALID => "My invalid message",
self::NOT_ALNUM => "foo",
self::STRING_EMPTY => "'%value%' is bar",
);
[ ... ]
}
Then instead of using Zend_Validate_Alnum, use My_Validate_Alnum as your validator. Custom validators are very simple to create.

How to make this Filter run after this Validator

I have an element. I want to add a custom validator and custom filter to it. The validator makes sure the input is one of several permitted values, then the filter adds some custom values to the input. This means I have to validate the original input first before running the filter. I do it in this order
$element = new Zend_Form_Element_Text('element');
$element->addValidator('PermittedValue', false);
$element->addFilter('TotalHyphen', false);
$this->addElement($element);
but this order isn't being respected. The filter runs first and changes the data, then the validator runs on the filtered data which means it always fails even for valid input. It seems from documentation that this is intentional
Note: Validation Operates On Filtered
Values Zend_Form_Element::isValid()
filters values through the provided
filter chain prior to validation. See
the Filters section for more
information.
How can I specify the order in which validators and filters run?
Sure seems like creating a custom element that supports post-validation filtering would be the way to go. How about this:
/**
* An element that supports post-validation filtering
*/
class My_Form_Element_PostValidateFilterable extends Zend_Form_Element_Text
{
protected $_postValidateFilters = array();
public function setPostValidateFilters(array $filters)
{
$this->_postValidateFilters = $filters;
return $this;
}
public function getPostValidateFilters()
{
return $this->_postValidateFilters;
}
public function isValid($value, $context = null)
{
$isValid = parent::isValid($value, $context);
if ($isValid){
foreach ($this->getPostValidateFilters() as $filter){
$value = $filter->filter($value);
}
$this->setValue($value);
}
return $isValid;
}
}
Usage would be something like this:
$elt = $form->addElement('PostValidateFilterable', 'myElement', array(
'label' => 'MyLabel',
'filters' => array(
'StringTrim',
// etc
),
'validators' => array(
'NotEmpty',
// etc
),
// here comes the good stuff
'postValidateFilters' => array(
new My_Filter_RunAfterValidateOne(),
new My_Filter_RunAfterValidateTwo(),
),
));
This keeps the validation and filtering in the form - keeping the controller thin.
Not tested, just a stab in the dark. And surely you could fatten/modify the API to add/remove filters by key, etc.
Whaddya think?
Maybe don't add the filter at all. Validate the content first in the controller, and then use the filter separately:
$request = $this->getRequest();
if ($request->isPost() && $form->isValid($request->getParams())) {
$filter = new Filter_Whatever();
$val = $filter->filter($request->getParam('element'));
... //call your model or whatever
}
I've never done this, but I suppose this (or something similar) might work.
Good point ! ,
AFAIK filters should or must run before validating the input :
from ZF docs
It's often useful and/or necessary to
perform some normalization on input
prior to validation. For example, you
may want to strip out all HTML, but
run your validations on what remains
to ensure the submission is valid. Or
you may want to trim empty space
surrounding input so that a
StringLength validator will use the
correct length of the input without
counting leading or trailing
whitespace characters.
but if and only if you are in case which can't solve mingos's answer must be the help
What you want to achieve is to change default behavior of how text element is being processed. Thus, I think you could create your own element (e.g. My_Form_Element_Text) that extends Zend_Form_Element_Text and overload its isValid() method.
Specifically you could just change second line in the orginal isValid() method, from $value = $this->getValue(); into $value = $this->getUnfilteredValue();. This way your validation will be performed using unfiltered values.

Categories