Yii2 - custom validation with db query - yii2

I am trying to make this custom validation works, but I am not getting anything at the moment. What seems to be the problem?
['password', function($attribute, $params){
$password = \Yii::$app->db
->createCommand("SELECT * FROM forbiddenPasswords WHERE password = '{$params}'")
->queryOne();
if($password)
$this->addError($attribute, 'This password is forbidden. Please try another.');
}],

$params contains validator parameters, not attribute,
you should correctly bind parameter in your query.
e.g. :
$count = Yii::$app->db->createCommand('SELECT COUNT(*) FROM forbiddenPasswords WHERE password = :password')
->bindValue(':password', $this->password)
->queryScalar();
if($count)
$this->addError($attribute, 'This password is forbidden. Please try another.');
Or you could create an ActiveRecord model for forbiddenPasswords and use unique validator to do the same...

I have checked that {$params} variable is for additional values. And if you want to validate password assign the value like this.
['password', function($attribute, $params){
$pass=$this->password;
$password = \Yii::$app->db
->createCommand("SELECT * FROM forbiddenPasswords WHERE password = '{$pass}'")
->queryOne();
if($password)
$this->addError($attribute, 'This password is forbidden. Please try another.');
}],

Don't write open password validator. It's unsecure!
In Yii2 you can use validatePassword method of Security component.
First store in database hash of password by setPassword method:
/**
*
* #param string $password WARNING! OPEN PASSWORD!
*/
public function setPassword($password)
{
$this->password_hash = Yii::$app->security->generatePasswordHash($password);
}
In model you should have method validatePassword:
/**
* #param string $password WARNING! OPEN PASSWORD!
*
* #return boolean
*/
public function validatePassword($password)
{
return Yii::$app->security->validatePassword($password, $this->password_hash);
}
Or, if you want use User model as form you can write this:
/**
* #inheritdoc
*/
public function rules()
{
return [
...
['password', 'validatePassword']
];
}
/**
* #param string $attribute attribute name
* #param array $params Additional params
*/
public function validatePassword($attribute, $params)
{
if (Yii::$app->security->validatePassword($this->$attribute, $this->password_hash) == false) {
$this->addError($attribute, Yii::t('frontend', 'Incorrect password'));
}
}

Related

Restricting controller action to creator of post in Yii2

Is there an easy way to restrict a controller action to the owner/creator of the post without using full blown RBAC?
Right now I'm doing this for every controller:
public function actionUpdate( $id ) {
$model = $this->findModel( $id );
if ( $model->user_id != Yii::$app->user->identity->id ) {
throw new NotFoundHttpException( 'The requested page does not exist.' );
}
}
But I think there must be a better way to restrict certain controllers to the users who created the $model thats being edited.
1) The recommended way is to use RBAC and rules. It's covered well in official docs in according dedicated section.
Example of rule that checks if author id matches current user id passed via params:
namespace app\rbac;
use yii\rbac\Rule;
/**
* Checks if authorID matches user passed via params
*/
class AuthorRule extends Rule
{
public $name = 'isAuthor';
/**
* #param string|integer $user the user ID.
* #param Item $item the role or permission that this rule is associated with
* #param array $params parameters passed to ManagerInterface::checkAccess().
* #return boolean a value indicating whether the rule permits the role or permission it is associated with.
*/
public function execute($user, $item, $params)
{
return isset($params['post']) ? $params['post']->createdBy == $user : false;
}
}
Then you need to tie it with existing permission (can be done in migration or with extensions):
$auth = Yii::$app->authManager;
// add the rule
$rule = new \app\rbac\AuthorRule;
$auth->add($rule);
// add the "updateOwnPost" permission and associate the rule with it.
$updateOwnPost = $auth->createPermission('updateOwnPost');
$updateOwnPost->description = 'Update own post';
$updateOwnPost->ruleName = $rule->name;
$auth->add($updateOwnPost);
// "updateOwnPost" will be used from "updatePost"
$auth->addChild($updateOwnPost, $updatePost);
// allow "author" to update their own posts
$auth->addChild($author, $updateOwnPost);
Then you can check if you user can update post like this:
use yii\web\ForbiddenHttpException;
use Yii;
public function actionUpdate($id)
{
$model = $this->findModel($id);
if (!Yii::$app->user->can('updatePost', ['post' => $model])) {
throw new ForbiddenHttpException('You are not allowed to edit this post');
}
...
}
Also note that in case you found model first and user has no access to edit it, logically it's better to throw 403 Forbidden exception rather than 404, since it's found, but not allowed for editing.
Don't forget to include rule like that in AccessControl behavior:
[
'allow' => true,
'actions' => ['update'],
'roles' => ['#'],
],
It means that update action of this controller can be only accessed by authorized users excluding guests.
2) If for some reason you don't want to use RBAC, you can use your approach:
use yii\web\ForbiddenHttpException;
public function actionUpdate($id)
{
$model = $this->findModel($id);
if ($model->user_id != Yii::$app->user->id ) {
throw new ForbiddenHttpException('You are not allowed to edit this post.');
}
...
}
To improve this you can abstract from this check by moving this logic to helper method:
namespace app\posts\components;
use Yii;
class PostPermission
{
/**
* #param $model Post
* #return boolean
*/
public static function allowedToUpdate($model)
{
return $model->user_id = Yii:$app->user->id;
}
}
Then call it like that:
use app\posts\components\PostPermission;
use yii\web\ForbiddenHttpException;
if (!PostPermission::allowedToUpdate($model) {
throw new ForbiddenHttpException('You are not allowed to edit this post.');
}
It's just an example, method doesn't have to be static, you can construct instance using $model.
You can just directly create method in Post model, but it's better to not pollute model with such logic.
3) Another alternative that I can advise is to restrict scope initially to current user when finding model:
use yii\web\NotFoundHttpException;
/**
* #param integer $id
* #return Post
* #throws NotFoundHttpException
*/
protected function findModel($id)
{
$model = Post::find(['id'=> $id, 'user_id' => Yii::$app->user->id])->one();
if ($model) {
return $model;
} else {
throw new NotFoundHttpException('This post does not exist.');
}
}
This can be improved for site administrators:
use yii\web\NotFoundHttpException;
/**
* #param integer $id
* #return Post
* #throws NotFoundHttpException
*/
protected function findModel($id)
{
$query = Post::find()->where(['id' => $id]);
if (!Yii::$app->user->is_admin) { // replace with your own check
$query->andWhere(['user_id' => Yii::$app->user->id]);
}
$model = $query->one();
if ($model) {
return $model;
} else {
throw new NotFoundHttpException('This post does not exist.');
}
}
Then you only write:
public function actionUpdate($id)
{
$model = $this->findModel($id);
...
}
That way in both cases (model not found and not allowed for editing by current user), 404 Not Found exception will be raised. From other side, nothing is wrong with that, because technically for this user this model does not exist (since he is not author of it).
We can use
AccessControlFilter
for restricting controller action instead of RBAC. This below code will give access to the actionUpdate if it is only pass the denyCallback.
use yii\filters\AccessControl;
class SiteController extends Controller
{
public function behaviors()
{
return [
'access' => [
'class' => AccessControl::className(),
'only' => ['update','delete'],
'rules' => [
[
'actions' => ['update'],
'allow' => false,
'denyCallback' => function ($rule, $action) { //PHP callable that should be called when this rule will deny the access.
//Write your logic here to deny the action
throw new \Exception('You are not allowed to access this page');
}
],
],
],
];
}
public function actionUpdate()
{
return $this->render('update');
}
}
For your reference https://github.com/yiisoft/yii2/blob/master/docs/guide/security-authorization.md

Symfony2 implement PATCH method to update user fileds

How can I update FOS USER user details using PATCH method. So when I pass partial details in Json, only these details are updated.
User entity
/**
* #ORM\Entity
* #ORM\Table(name="user")
*/
abstract class User extends BaseUser
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #var string]
* #ORM\Column(name="first_name", type="string", length=255, nullable=true)
*/
private $firstName;
/**
* #var string
* #ORM\Column(name="last_name", type="string", length=255, nullable=true)
*/
private $lastName;
// getters and setters are here
}
For example. I have user Jack Nickolson and my json is:
{"firstName": "John"}
Only first name is updated.
This is my controller. How do I set new parameters to user, without specifing which parameters?
/**
* #Route("/update", name="api_user_update")
* #Security("has_role('ROLE_USER')")
* #Method("PATCH")
*/
public function updateAction(Request $request){
$jsonContent = $request->getContent();
$params = json_decode($jsonContent);
$em = $this->getDoctrine()->getEntityManager();
$response = new JsonResponse();
$user = $this->getUser();
// do something to update user details
$this->get('fos_user.user_manager')->updateUser($user);
$em->persist($user);
$em->flush();
$response->setContent([
"status" => "user ". $user->getUsername() ." is updated"
]);
return $response;
}
UPDATE
I tried to use an answer below, so this what I have now
public function updateAction(Request $request){
$em = $this->getDoctrine()->getEntityManager();
$response = new JsonResponse();
$user = $this->getUser();
$editForm = $this->createForm('CoreBundle\Form\UserType', $user);
$requestData = $request->getContent();
$requestData = json_encode($requestData, true);
$editForm->submit($requestData, false);
$this->get('fos_user.user_manager')->updateUser($user);
$em->persist($user);
$em->flush();
$response->setContent([
"status" => "user ". $user->getUsername() ." is updated"
]);
return $response;
}
Well, my entity was not updated. What am I doing wrong?
You need to prepare and create UserType form for partial updated.
Then, when processing a submitted form, you'll need to pass a false $clearMissing option to your form's submit method call in the controller:
$form->submit($request, false);
Thanks to that option the form component will only update the fields passed from the request.
and then flush user:
$em->flush();
Also if you want send data like:
{"firstName": "John"}
your UserType form should have method:
public function getBlockPrefix()
{
return "";
}
You can also use nice and powerfull https://github.com/FriendsOfSymfony/FOSRestBundle

FOSRest Symfony POST using json

I'm new with the symfony framework. I'm trying to create webservices with FOSRest bundle but I had a problems when I tried to implement the POST method for one entity with json.
Test:
public function testJsonPostNewTesteAction(){
$this->client->request(
'POST',
'/api/teste/new',
array(),
array(),
array(
'Content-Type' => 'application/json',
'Accept' => 'application/json'
),
'{"Teste":{"title":"O teu title"}}'
);
$response = $this->client->getResponse();
$this->assertJsonResponse($response, Codes::HTTP_CREATED);
}
Controller:
/**
*
* #Config\Route(
* "/teste/new",
* name="postTestNew"
* )
* #Config\Method({"POST"})
*
* #param Request $request the request object
*
* #return View|Response
*/
public function postNewTeste(Request $request){
return $this->processFormTest($request);
}
/**
* Method for create the process form to Advertisements
*
* #param Request $request
*
* #return mixed
*/
private function processFormTest(Request $request){
$form = $this->createForm(
new TesteType(),
new Teste()
);
$form->bind($request);
//$from->handleRequest($request);
if ($form->isValid()) {
$test = $form->getData();
return $test;
}
return View::create($form, Codes::HTTP_BAD_REQUEST);
}
The problem is when I use the handleRequest(), the method isValid() returns false because the form didn't submit. So I try to change the handleRequest to the bind method. In the last case, the method isValid() returns true but the method getData() returns a null object.
I don't know if the problem is the type form class bellow.
Type Form:
/**
* The constant name to that type
*/
const TYPE_NAME = "Teste";
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options){
parent::buildForm($builder, $options);
$builder->add("title", ValidationType::TEXT);
}
/**
* {#inheritdoc}
*/
public function setDefaultOptions(OptionsResolverInterface $resolver){
$resolver->setDefaults(
array(
'csrf_protection' => false,
'cascade_validation' => true,
'data_class' => 'oTeuGato\DatabaseBundle\Entity\Teste'
)
);
}
/**
* Returns the name of this type.
*
* #return string The name of this type
*/
public function getName(){
return AdvertisementType::TYPE_NAME;
}
I need to POST the entity with both ways. Anyone sugest anything for my problem?
Thanks for the patience!
Got the same issue with the form binding, the only way i found to solve it was to set an empty string to the form getName function:
/**
* Returns the name of this type.
*
* #return string The name of this type
*/
public function getName(){
return '';
}
And i would suggest to use the handleRequest method when you bind your data because the bind method is deprecated:
private function processFormTest(Request $request){
$form = $this->createForm(
new TesteType(),
new Teste()
);
$from->handleRequest($request);
if ($form->isValid()) {
$test = $form->getData();
return $test;
}
return View::create($form, Codes::HTTP_BAD_REQUEST);
}
It looks more like a hack but seems like it's the only way for now:
https://github.com/FriendsOfSymfony/FOSRestBundle/issues/585
I am not entirely sure but I think it's because you're sending the data with: 'Content-Type' => 'application/json'
in stead of 'Content-Type' => 'application/x-www-form-urlencoded'
Or at least I think that's the reason why the handleRequest is not doing it's thing.

Update discriminator column Doctrine2 with Symfony2

I have an entity called User which has inheritance for Student, Professional and Business.
When a user is registered, is only a User but they must update their profile and choose which kind of user is, I have a form which handles this, a controller which gets the form data, but I can't update the discriminator field type with $userEntity->setType()
This is my mapping stuff
class User
{
const TYPE_BASIC = "Basico";
const TYPE_STUDENT = "Estudiante";
const TYPE_PROFESSIONAL = "Profesional";
const TYPE_BUSINESS = "Empresa";
protected $type = self::TYPE_BASIC;
public function getType()
{
return self::TYPE_BASIC;
}
public function setType($type)
{
$this->type = $type;
}
class Student extends User
{
protected $type = self::TYPE_STUDENT;
And then Professional and Business just like Student (changing const)
<entity name="User" table="user_base" inheritance-type="JOINED">
<discriminator-column name="type" type="string"/>
<discriminator-map>
<discriminator-mapping value="Basico" class="User"/>
<discriminator-mapping value="Estudiante" class="Student"/>
<discriminator-mapping value="Profesional" class="Professional"/>
<discriminator-mapping value="Empresa" class="Business"/>
</discriminator-map>
the child tables are named user_xxx where xxx = Student/Professional/Business
And this is my controller
if($form->isValid())
{
$em = $this->getDoctrine()->getManager();
$data = $form->all();
$type = $data['type']->getData();
$email = $data['email']->getData();
$profile = $data['profile']->all();
$name = $profile['name']->getData();
$lastName = $profile['lastName']->getData();
$birth = $profile['birth']->getData();
$profileEntity = new Profile();
$profileEntity->setBirth($birth);
$profileEntity->setName($name);
$profileEntity->setLastName($lastName);
$profileEntity->setUser($user);
$em->persist($profileEntity);
ladybug_dump($type);
$userEntity = $em->getRepository('User')->find($user);
$userEntity->setProfile($profileEntity);
$userEntity->setType($type);
if($user->getEmail() != $email)
$userEntity->setEmail($email);
$em->persist($userEntity);
$em->flush();
}
Everything is persisted but type field, which remains it's original data. I know when I change discriminator column I need to create a new row inside it's child element, but first I want to know how to change the discriminator column.
it is possible if you use this custom bit of code in the Form of a Trait which you can use inside a Repository.
The Trait:
namespace App\Doctrine\Repository;
use App\Exception\InvalidDiscriminatorClassException;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\DBALException;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Mapping\ClassMetadata;
/**
* Discriminator Trait
*/
trait DiscriminatorTrait
{
/**
* #return ClassMetadata
*/
abstract public function getClassMetadata();
/**
* #return EntityManager
*/
abstract public function getEntityManager();
/**
* Update Discriminator Column
*
* #param integer $id
* #param string $class
* #return boolean
* #throws InvalidDiscriminatorClassException
*/
private function updateDiscriminatorColumn($id, $class)
{
/* #var ClassMetadata $classMetadata */
$classMetadata = $this->getClassMetadata();
if (!in_array($class, $classMetadata->discriminatorMap)) {
throw new InvalidDiscriminatorClassException($class);
}
$identifier = $classMetadata->fieldMappings[$classMetadata->identifier[0]]["columnName"];
$column = $classMetadata->discriminatorColumn["fieldName"];
$value = array_search($class, $classMetadata->discriminatorMap);
/* #var Connection $connection */
$connection = $this->getEntityManager()->getConnection();
try {
$connection->update(
$classMetadata->table["name"],
[$column => $value],
[$identifier => $id]
);
}
catch (DBALException $e) {
return false;
}
return true;
}
}
According to the Doctrine documentation on Inheritance mapping, it is not possible to either get or set the type. You may wish take advantage of PUGXMultiUserBundle, which readily handles the mapping. This bundle also makes it possible for your users to register with the appropriate profile.

How to declare a EventSubscriber in config.yml using bazinga_geocoder.geocoder service?

I am planning to make a reverse geocoding based on the BazingaGeocoderBundle. A simple way to do that is write this simple code in the controller:
$result = $this->container
->get('bazinga_geocoder.geocoder')
->using('google_maps')
->reverse(48.79084170157100,2.42479377175290);
return $this->render("MinnAdsBundle:Motors:test.html.twig",
array('result'=>var_dump($result)));
Until here, things are going well.
My objective is to make the code nicer & resuable. So, I used this article to write my own GeocoderEventSubscriber as describer below:
<?php
namespace Minn\AdsBundle\Doctrine\Event;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\PreUpdateEventArgs;
use Doctrine\ORM\Event\LifecycleEventArgs;
//use Geocoder\Provider\ProviderInterface;
use Bazinga\Bundle\GeocoderBundle\Geocoder\LoggableGeocoder;
/**
* Subscribes to Doctrine prePersist and preUpdate to update an
* the address components of a MotorsAds entity
*
* #author majallouli
*/
class MotorsAdsGeocoderEventSubscriber implements EventSubscriber {
protected $geocoder;
public function __construct(LoggableGeocoder $geocoder){
$this->geocoder = $geocoder;
}
/**
* Specifies the list of events to listen
*
* #return array
*/
public function getSubscribedEvents(){
return array(
'prePersist',
'preUpdate',
);
}
/**
* Sets a new MotorsAds's address components if not present
*
* #param LifecycleEventArgs $eventArgs
*/
public function prePersist(LifecycleEventArgs $eventArgs){
$motorsAds = $eventArgs->getEntity();
if($motorsAds instanceof \Minn\AdsBundle\Entity\MotorsAds){
if( !$motorsAds->getCountry()){
$em = $eventArgs->getEntityManager();
$this->geocodeMotorsAds($motorsAds,$em);
}
}
}
/**
* Sets an updating MotorsAds's address components if not present
* or any part of address updated
*
* #param PreUpdateEventArgs $eventArgs
*/
public function preUpdate(PreUpdateEventArgs $eventArgs){
$motorsAds = $eventArgs->getEntity();
if($motorsAds instanceof \Minn\AdsBundle\Entity\MotorsAds){
if( !$motorsAds->getCountry() ){
$em = $eventArgs->getEntityManager();
$this->geocodeMotorsAds($motorsAds,$em);
$uow = $em->getUnitOfWork();
$meta = $em->getClassMetadata(get_class($motorsAds));
$uow->recomputeSingleEntityChangeSet($meta, $motorsAds);
}
}
}
/**
* Geocode and set the MotorsAds's address components
*
* #param type $motorsAds
*/
private function geocodeMotorsAds($motorsAds,$em){
$result = $this->geocode
->using('google_maps')
->reverse($motorsAds->getLat(),$motorsAds->getLng());
$motorsAds->setCountry(
$em->getRepository("MinnAdsBundle:Country")->findCountryCode($result['countryCode']));
}
}
After that, I declared my EventSubscriber as a service:
services:
# ...
geocoder_motorsads.listener:
class: Minn\AdsBundle\Doctrine\Event\MotorsAdsGeocoderEventSubscriber
arguments: [#bazinga_geocoder.geocoder] # almost sure that the error is here!!
tags:
- { name: doctrine.event_subscriber }
Actually, I get this error:
ContextErrorException: Notice: Undefined property: Minn\AdsBundle\Doctrine\Event\MotorsAdsGeocoderEventSubscriber::$geocode in /home/amine/NetBeansProjects/tuto/src/Minn/AdsBundle/Doctrine/Event/MotorsAdsGeocoderEventSubscriber.php line 78
I am almost sure that error is in the declaration of arguments of the EventSubscriber. Is it #bazinga_geocoder.geocoder?
Thank you for your help!
Your property is $this->geocoder but you're calling $this->geocode, you're spelling it wrong.