How to do Kohana Validation of $_serialize_column inside ORM - json

The validation on Kohana ORM is done using rules
function rules()
{
return array(
'username' => array(
array('not_empty'),
array(array($this, 'availability')),
)
);
}
I'm struggling to validate a JSON encoded column using $_serialize_columns.
class Model_Admin extends ORM {
protected $_belongs_to = array();
protected $_has_many = array(
'plans' => array(),
'groups' => array(),
'transactions' => array(),
'logins' => array()
);
protected $_serialize_columns = array('data');
/**
* #param array $data
* #param Validation $validation
*
* #return bool
*/
public function data($data, $validation)
{
return
Validation::factory(json_decode($data, TRUE))
// ... rules ...
->check();
}
public function rules()
{
return array(
'data' => array(
array(array($this, 'data'), array(':value',':validation')
)
);
}
}
the array that gets encoded is:
array(
'name' => '',
'address' => '',
'phone' => '',
'postalcode' => ''
);
the data method receives the json encoded data, because the ORM runs the filters before doing the validation, so I need to convert it back to an associative array, then create a new validation object to check specifically for the content of that array. Because I can't merge Validation rules from another Validation instance

Updated Answer
The use of a second validation object is necessary since save() causes the internal model validation object to be checked. This means that rules added to the validation object being checked from a validation rule will be ignored (Validation->check() imports the rules into local scope before looping).
Since the data itself is technically another object (in the sense of object relationships, it has its own dataset that needs validation) the ideal solution would be to find a way to create a real model that saves the data.
There are numerous other benefits to saving data with proper database column definitions, not least if you need to perform data property lookups, make in-situ changes etc. (which would otherwise require unserializing the data column, potetnailly in all rows).
There are some alternatives, but they feel like kludges to me:
Create a model that represents the data object and add rules to it, using check() to validate the data (problem: will require a lot of maintenance, no real-world table means columns must be manually defined).
Set the data as real columns in the Admin model, and use a filter that will convert it into the data column on set (problem: again, must manually define the columns and exclude the additional columns from the save operation).
I hope this is of some use.
Original Answer
The Kohana ORM save() method permits the inclusion of an "extra" validation object, which is merged into the main ORM validation object namespace.
This is documented briefly here.
If I have understood correctly, I think you are looking to do something like this:
// another script, e.g., a controller
// Create the model
$admin = ORM::factory('Admin');
// $data = the data as an array, before serialization ...
$extra_validation = Validation::factory($data)
// add ->rule() calls here, but DO NOT chain ->check()
;
// Set $data in the model if it is going to be saved, e.g., $admin->data = $data;
// Set other data... e.g., $admin->foo = 'bar';
// Save the model
try {
$admin->save($extra_validation);
}
catch (ORM_Validation_Exception $e)
{
// Manipulate the exception result
}
While in this example you must still create another validation object, you are now able to catch all exceptions in a single block. I would recommend using var_dump() or similar on $e->errors() to check the namespace if you are using i18n messages to provide a human-readable error message. You should find that a namespace called "_external" has been created in the response.

Related

How to implement Filtering on YII restful GET api?

I am working on Restful APIs of Yii.
My controller name is ProductsController and the Model is Product.
When I call API like this GET /products, I got the listing of all the products.
But, now I want to filter the records inside the listing API.
For Example, I only want those records Which are having a product name as chairs.
How to implement this?
How to apply proper filtering on my Rest API. I am new to this. So, I have no idea how to implement this. I also followed their documentation but unable to understand.
May someone please suggest me a good example or a way to achieve this?
First of all you need to have validation rules in your model as usual.
Then it's the controllers job and depending on the chosen implementation I can give you some hints:
If your ProductsController extends yii\rest\ActiveController
Basically the easiest way because almost everything is already prepared for you. You just need to provide the $modelClass there and tweak actions() method a bit.
public function actions()
{
$actions = parent::actions();
$actions['index']['dataFilter'] = [
'class' => \yii\data\ActiveDataFilter::class,
'searchModel' => $this->modelClass,
];
return $actions;
}
Here we are modifying the configuration for IndexAction which is by default responsible for GET /products request handling. The configuration is defined here and we want to just add dataFilter key configured to use ActiveDataFilter which processes filter query on the searched model which is our Product. The other actions are remaining the same.
Now you can use DataProvider filters like this (assuming that property storing the product's name is name):
GET /products?filter[name]=chairs will return list of all Products where name is chairs,
GET /products?filter[name][like]=chairs will return list of all Products where name contains word chairs.
If your ProductsController doesn't extend yii\rest\ActiveController but you are still using DataProvider to get collection
Hopefully your ProductsController extends yii\rest\Controller because it will already benefit from serializer and other utilities but it's not required.
The solution is the same as above but now you have to add it by yourself so make sure your controller's action contains something like this:
$requestParams = \Yii::$app->getRequest()->getBodyParams(); // [1]
if (empty($requestParams)) {
$requestParams = \Yii::$app->getRequest()->getQueryParams(); // [2]
}
$dataFilter = new \yii\data\ActiveDataFilter([
'searchModel' => Product::class // [3]
]);
if ($dataFilter->load($requestParams)) {
$filter = $dataFilter->build(); // [4]
if ($filter === false) { // [5]
return $dataFilter;
}
}
$query = Product::find();
if (!empty($filter)) {
$query->andWhere($filter); // [6]
}
return new \yii\data\ActiveDataProvider([
'query' => $query,
'pagination' => [
'params' => $requestParams,
],
'sort' => [
'params' => $requestParams,
],
]); // [7]
What is going on here (numbers matching the code comments):
We are gathering request parameters from the body,
If these are empty we take them from the URL,
We are preparing ActiveDataFilter as mentioned above with searched model being the Product,
ActiveDataFilter object is built using the gathered parameters,
If the build process returns false it means there is an error (usually unsuccessful validation) so we return the object to user to see list of errors,
If the filter is not empty we are applying it to the database query for Product,
Finally we are configuring ActiveDataProvider object to return the filtered (and paginated and sorted if applicable) collection.
Now you can use DataProvider filters just as mentioned above.
If your ProductsController doesn't use DataProvider to get collection
You need to create your custom solution.

Integers are marked as dirty attributes no matter what

I need to check if a model has been updated and what attributes have changed when saving.
I'm using dirtyAttributes and filter intval as the docs suggests.
The values are coming from an API and are type-cast as they come in, so in theory the filter is redundant.
Model rules
public function rules()
{
return [
[['contract_date', 'order_date'], 'integer'],
[['contract_date', 'order_date'], 'filter', 'filter' => 'intval'],
];
}
This is some of the code currently running:
// Add the changed status variables to the job log
$dirty_attributes = array_keys($model->dirtyAttributes);
if($model->save()) foreach ($dirty_attributes as $attribute)
{
$data[$attribute] = $model->getOldAttribute($attribute).' ('.gettype($model->getOldAttribute($attribute)).')'. ' => '. $model->$attribute.' ('.gettype($model->$attribute).')';
}
var_dump($data);
This produces:
["contract_date"]=>
string(44) "1559669638 (integer) => 1559669638 (integer)"
["order_date"]=>
string(44) "1559669638 (integer) => 1559669638 (integer)"
There is probably something obvious I'm missing, but I can understand what.
After saving model all "oldAttributes" are updated to store new values so comparing them like you do makes no sense. If you want to check which attributes have been changed after saving you can override afterSave() method in your model like:
public function afterSave($insert, $changedAttributes)
{
// $changedAttributes -> this is it
parent::afterSave(); // call parent to trigger event
}
or listen for ActiveRecord::EVENT_AFTER_INSERT / ActiveRecord::EVENT_AFTER_UPDATE event where this data is also passed.

How to get tablename from Event in Behavior

I'm writing a behavior in CakePHP 3.0.11, and I can var_dump() and Log::debug() the $event object in my behavior's beforeSave() method, and can see the object data, but cannot access the object's properties.
I am simply trying to get the table (alias, className, tablename, anything) name from the object.
I would like to do something like this in my Behavior:
public function beforeSave(Event $event, Entity $entity)
{
$table = $event->_alias;
// etc.
}
I tried the event's subject() method which extracts the table object from the event,
$table = $event->subject();
When I var_dump or debug the object returned, it shows:
Debug: App\Model\Table\CompaniesTable Object
(
[registryAlias] => Companies
[table] => companies
[alias] => Companies
[entityClass] => App\Model\Entity\Company
[associations] => Array
(
[0] => defaultshippingusers
(...)
[defaultConnection] => default
[connectionName] => default
)
But I cannot access 'table', 'alias', etc. from my $table object.
When I do, I get a fatal error:
Table Companies is not associated with 'alias'
Is there a simple way to get the Table name from the $event object in a behavior?
Dumping objects does not necessarily give you a representation of the objects structure, but custom formatted debug information, defined via the magic __debugInfo() method.
https://github.com/cakephp/cakephp/blob/3.0.11/src/ORM/Table.php#L2190
Table classes do not have table or alias properties, but methods with the same name, just have a look at the Cookbook and the API docs.
$alias = $event->subject()->alias();
Cookbook > Database Access & ORM > Table Objects > Basic Usage
API > \Cake\ORM\Table
API > \Cake\ORM\Table::alias()
API > \Cake\ORM\Table::table()
...
You can also get the table name or alias directly from the behaviour without using the $event object:
$this->getTable()->table();
$this->getTable()->alias();
This is useful if you add your own functions to the behaviour which don't pass in the $event.

Preventing malicious users update data at add action

Here is a basic add action:
public function add()
{
$article = $this->Articles->newEntity();
if ($this->request->is('post')) {
$article = $this->Articles->patchEntity($article, $this->request->data);
if ($this->Articles->save($article)) {
$this->Flash->success('Success.');
return $this->redirect(['action' => 'index']);
} else {
$this->Flash->error('Fail.');
}
}
$this->set(compact('article'));
}
If a malicious user injects at form a field with name id and set the value of this field to 2. Since the user do that the id value will be in $this->request->data so at $this->Articles->patchEntity($article, $this->request->data) this id will be patched and at $this->Articles->save($article) the record 2 will be updated instead of create a new record??
Depends.
Entity::$_accessible
If you baked your models, then this shouldn't happen, as the primary key field will not be included in the entities _accessible property, which defines the fields that can be mass assigned when creating/patching entities. (this behavior changed lately)
If you baked your models, then this shouldn't happen, as the primary key field(s) will be set to be non-assignable in the entities _accessible property, which means that these the fields cannot be set via mass assignment when creating/patching entities.
If you didn't baked your models and haven't defined the _accessible property, or added the primary key field to it, then yes, in case the posted data makes it to the patching mechanism, then that is what will happen, you'll be left with an UPDATE instead of an INSERT.
The Security component
The Security component will prevent form tampering, and reject requests with modified forms. If you'd use it, then the form data wouldn't make it to the add() method in the first place.
There's also the fieldList option
The fieldList option can be used when creating/patching entities in order to specifiy the fields that are allowed to be set on the entity. Sparse out the id field, and it cannot be injected anymore.
$article = $this->Articles->patchEntity($article, $this->request->data, [
'fieldList' => [
'title',
'body',
//...
]
]);
And finally, validation
Validation can prevent injections too, however that might be considered a little wonky. A custom rule that simply returns false would for example do it, you could create an additional validator, something like
public function validationAdd(Validator $validator) {
return
$this->validationDefault($validator)
->add('id', 'mustNotBePresent', ['rule' => function() {
return false;
}]);
}
which could then be used when patching the entity like
$article = $this->Articles->patchEntity($article, $this->request->data, [
'validate' => 'add'
]);

GET parameters validation

Every Yii2 guide/tutorial that I have come across ignores the validation of GET parameters. I'm wondering why.
To give an example, take a look at this code:
public function actionView($id)
{
/* #var $model ActiveRecord */
$model = Model::findOne($id);
if ($model) {
return $this->render('view', ['model' => $model]);
} else {
throw new \yii\web\NotFoundHttpException();
}
}
I understand that if you pass invalid argument to findOne() method, it will just return null and nothing bad happens. But is this really the best practice? I have always tried to be very careful with user input and the way I see it, user input should be validated immediately before performing any operations such as DB calls. Even if it's GET data, not just POST data.
public function actionView($id)
{
/* #var $model yii\base\DynamicModel */
$model = DynamicModel::validateData(['id' => $id], [
'idValidation' => ['id', integer]
]);
if ($model->hasErrors()) {
throw new \yii\web\BadRequestHttpException();
}
/* #var $model yii\db\ActiveRecord */
$model = Model::findOne($id);
if ($model) {
return $this->render('view', ['model' => $model]);
} else {
throw new \yii\web\NotFoundHttpException();
}
}
What do you think? Is my approach reasonable or overkill and unnecessary?
If you're using action parameters, you don't need to validate this parameters again (unless you have specific reason for it, like closed dictionary of allowed values, but I guess this is not the case). If your action uses signature like actionView($id) Yii will ensure few things before further processing of action:
$_GET['id'] exist, so $id will never be null. If someone will try to call this action without id value in GET, he will get BadRequestHttpException exception without calling action.
$_GET['id'] is a scalar. It means that if someone will try to pass array as id, he will get BadRequestHttpException exception without calling action.
So at this point in action you may be sure that $id is string. This is enough for findOne() safety. Even if you expect integer and someone pass blablabla as $id, it does not matter - he will get NotFoundHttpException anyway since there is no record with blablabla as id (this is impossible - blablabla in not a valid integer) - there is no need for extra check here. So default examples generated by Gii or from Yii documentation are safe. So your approach is a overkill and it is completely unnecessary.
Situation may change when $id can be array, since array allows much more powerful syntax. You need to take extra attention when:
You're explicitly allowing array as action param: actionView(array $id).
You're not using action params and using $_GET params directly: $id = $_GET['id'] or $id = Yii::$app->request->get('id') - in these cases $id can be array even if you're expecting scalar.
In this case $id value may be quite surprising. For example attacker may pass multiple IDs even if you're expecting single ID. Or filter by specified field instead of primary key, by passing ['email' => 'user#example.com'] as a $id - users will be searched by email field (or any other) even if intention is to filter only by ID. In such cases you should validate this array to make sure that it contains only expected values.
In older version this also allows for SQL Injection, since columns names (keys in array) were not escaped (this is still valid for where()). See 2.0.15 release announcement with some explanation.