in a function of my controller, I initialize a form that I pass in parameter to a view. The form must then redirect to another action of my controller, like this:
Controller : index()
/**
* #Route("/validation/absences", name="validation_index")
*/
public function index(PaginatorInterface $paginator, Request $request, AbsenceService $absenceService)
{
$refusAbsence = new Absence();
$formRefus = $this->createForm(RefusAbsenceType::class, $refusAbsence);
$formRefus->handleRequest($request);
return $this->render('validation/index.html.twig', [
"formRefus" => $formRefus->createView(),
]);
My form action goes to this function :
/**
* Refuser une demande d'absence
*
* #Route("validation/absences/refuser/{id}", name="validation_refuser")
*
* #param Absence $absence
* #return void
*/
public function refuser(Request $request, Absence $absence)
{
$token = $request->get('refus_absence')['_token'];
if (!$this->isCsrfTokenValid('refus_absence__token', $token)) {
throw new \Symfony\Component\Security\Core\Exception\AccessDeniedException('Accès interdit');
}
$commentaire = $request->get('refus_absence')['commentaire'];
dd($commentaire);
}
I get my token back with the request, but I can not get it to be validated. I still have the mistake.
Yet on Symfony's documentation, they say:
if ($this->isCsrfTokenValid('token_id', $submittedToken)) {
// ... do something, like deleting an object
}
And in my HTML, I've :
<input type="hidden" id="refus_absence__token" name="refus_absence[_token]" value="7bbockF5tz3r7Ne9f6dQB7Y5YMcwd1QRES4vHrhQEQE">
in your receiving function, just recreate the form:
$form = $this->createForm(RefusAbsenceType::class, new Absence());
$form->handleRequest($request);
// also checks csrf, it is enabled globally, otherwise, recreate parameters
// in the createForm call.
if($form->isSubmitted() && $form->isValid()) {
$absence = $form->getData();
// do whatever ... persist and stuff ...
}
Related
I know, this is a complex case but maybe one of you might have an idea on how to do this.
Concept
I have the following process in my API:
Process query string parameters (FormRequest)
Replace key aliases by preferred keys
Map string parameters to arrays if an array ist expected
Set defaults (including Auth::user() for id-based parameters)
etc.
Check if the user is allowed to do the request (Middleware)
Using processed (validated and sanitized) query params
→ otherwise I had to do exceptions for every possible alias and mapping as well as checking if the paramter is checked and that doesn't seem reasonable to me.
Problem
Nevertheless, if you just assign the middleware via ->middleware('middlewareName') to the route and the FormRequest via dependency injection to the controller method, first the middleware is called and after that the FormRequest. As described above, that's not what I need.
Solution approach
I first tried dependency injection at the middleware but it didn't work.
My solution was to assign the middleware in the controller constructor. Dependency injection works here, but suddenly Auth::user() returns null.
Then, I came across the FormRequest::createFrom($request) method in \Illuminate\Foundation\Providers\FormRequestServiceProvider.php:34 and the possibility to pass the $request object to the middleware's handle() method. The result looks like this:
public function __construct(Request $request)
{
$middleware = new MyMiddleware();
$request = MyRequest::createFrom($request);
$middleware->handle($request, function() {})
}
But now the request is not validated yet. Just calling $request->validated() returns nothing. So I digged a little deeper and found that $resolved->validateResolved(); is done in \Illuminate\Foundation\Providers\FormRequestServiceProvider.php:30 but that doesn't seem to trigger the validation since it throws an exception saying that this method cannot be called on null but $request isn't null:
Call to a member function validated() on null
Now, I'm completely stumped. Does anyone know how to solve this or am I just doing it wrong?
Thanks in advance!
I guess, I figured out a better way to do this.
My misconception
While middleware is doing authentication, I was doing authorization there and therefore I have to use a Gate
Resulting code
Controller
...
public function getData(MyRequest $request)
{
$filters = $request->query();
// execute queries
}
...
FormRequest
class MyRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize()
{
return Gate::allows('get-data', $this);
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
// ...
];
}
/**
* Prepare the data for validation.
*
* #return void
*/
protected function prepareForValidation()
{
$this->replace($this->cleanQueryParameters($this->query()));
}
private function cleanQueryParameters($queryParams): array
{
$queryParams = array_filter($queryParams, function($param) {
return is_array($param) ? count($param) : strlen($param);
});
$defaultStartDate = (new \DateTime())->modify('monday next week');
$defaultEndDate = (new \DateTime())->modify('friday next week');
$defaults = [
'article.created_by_id' => self::getDefaultEmployeeIds(),
'date_from' => $defaultStartDate->format('Y-m-d'),
'date_to' => $defaultEndDate->format('Y-m-d')
];
$aliases = [
// ...
];
$mapper = [
// ...
];
foreach($aliases as $alias => $key) {
if (array_key_exists($alias, $queryParams)) {
$queryParams[$key] = $queryParams[$alias];
unset($queryParams[$alias]);
}
}
foreach($mapper as $key => $fn) {
if (array_key_exists($key, $queryParams)) {
$fn($queryParams, $key);
}
}
$allowedFilters = array_merge(
Ticket::$allowedApiParameters,
array_map(function(string $param) {
return 'article.'.$param;
}, TicketArticle::$allowedApiParameters)
);
$arrayProps = [
// ..
];
foreach($queryParams as $param => $value) {
if (!in_array($param, $allowedFilters) && !in_array($param, ['date_from', 'date_to'])) {
abort(400, 'Filter "'.$param.'" not found');
}
if (in_array($param, $arrayProps)) {
$queryParams[$param] = guarantee('array', $value);
}
}
return array_merge($defaults, $queryParams);
}
}
Gate
class MyGate
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Auth\Access\Response|Void
* #throws \Symfony\Component\HttpKernel\Exception\HttpException
*/
public function authorizeGetDataCall(User $user, MyRequest $request): Response
{
Log::info('[MyGate] Checking permissions …');
if (in_array(LDAPGroups::Admin, session('PermissionGroups', []))) {
// no further checks needed
Log::info('[MyGate] User is administrator. No further checks needed');
return Response::allow();
}
if (
($request->has('group') && !in_array(Group::toLDAPGroup($request->get('group')), session('PermissionGroups', []))) ||
$request->has('owner.department') && !in_array(Department::toLDAPGroup($request->query('owner.department')), session('PermissionGroups', [])) ||
$request->has('creator.department') && !in_array(Department::toLDAPGroup($request->query('creator.department')), session('PermissionGroups', []))
) {
Log::warning('[MyGate] Access denied due to insufficient group/deparment membership', [ 'group/department' =>
$request->has('group') ?
Group::toLDAPGroup($request->get('group')) :
($request->has('owner.department') ?
Department::toLDAPGroup($request->query('owner.department')) :
($request->has('creator.department') ?
Department::toLDAPGroup($request->query('creator.department')) :
null))
]);
return Response::deny('Access denied');
}
if ($request->has('customer_id') || $request->has('article.created_by_id')) {
$ids = [];
if ($request->has('customer_id')) {
$ids = array_merge($ids, $request->query('customer_id'));
}
if ($request->has('article.created_by_id')) {
$ids = array_merge($ids, $request->query('article.created_by_id'));
}
$users = User::find($ids);
$hasOtherLDAPGroup = !$users->every(function($user) {
return in_array(Department::toLDAPGroup($user->department), session('PermissionGroups', []));
});
if ($hasOtherLDAPGroup) {
Log::warning('[MyGate] Access denied due to insufficient permissions to see specific other user\'s data', [ 'ids' => $ids ]);
return Response::deny('Access denied');;
}
}
if ($request->has('owner.login') || $request->has('creator.login')) {
$logins = [];
if ($request->has('owner.login')) {
$logins = array_merge(
$logins,
guarantee('array', $request->query('owner.login'))
);
}
if ($request->has('creator.login')) {
$logins = array_merge(
$logins,
guarantee('array', $request->query('creator.login'))
);
}
$users = User::where([ 'samaccountname' => $logins ])->get();
$hasOtherLDAPGroup = !$users->every(function($user) {
return in_array(Department::toLDAPGroup($user->department), session('PermissionGroups', []));
});
if ($hasOtherLDAPGroup) {
Log::warning('[MyGate] Access denied due to insufficient permissions to see specific other user\'s data', [ 'logins' => $logins ]);
return Response::deny('Access denied');
}
}
Log::info('[MyGate] Permission checks passed');
return Response::allow();
}
}
I have the following controller method which creates a new Category entity and persists it to the database:
/**
* #param Request $request
*
* #return array
*
* #Route("/admin/category/new", name="_admin_category_new")
* #Method({"GET", "POST"})
* #Template("Admin/category_new.html.twig")
*/
public function newCategoryAction(Request $request)
{
$category = new Category();
$form = $this->createForm(CategoryType::class, $category);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($category);
if (!$category->getSlug()) {
$category->setSlug();
}
if ($category->getFile() != null) {
$um = $this->get('stof_doctrine_extensions.uploadable.manager');
$um->markEntityToUpload($category, $category->getFile());
}
$em->flush();
$this->addFlash('success', 'Category successfully created');
$this->redirect($this->generateUrl('_admin_category', array('page' => 1)));
}
return array('form' => $form->createView());
}
Upon successful completion, it's supposed to redirect the user to a different URL. Instead, it just re-displays the current page/form. Any ideas? The route _admin_category does exist, and it is working:
$ bin/console debug:router
...
_admin_category GET ANY ANY /admin/category/{page}
...
And my Category entities are being saved to the DB properly.
You should return redirect response try
return $this->redirectToRoute('_admin_category', ['page' => 1]);
Redirect method creates an object of RedirectResponse class, and it needs to be returned as response. Moreover, you don't have to use redirect + generateUrl you can just use redirectToRoute method which is shortcut for that.
Also I'd suggest wrapping flush with try/catch
For more see docs
repository with issue
I have a form for entity User with email field:
->add('email', EmailType::class, [
'constraints' => [
new NotBlank(),
new Email([
'checkMX' => true,
])
],
'required' => true
])
when i'm editing email to something like test#gmail.com1 and submit form it shows me error "This value is not a valid email address." THat's ok, but after that symfony populate wrong email into token and when i'm going to any other page or just reload page, i'm getting this:
WARNING security Username could not be found in the selected user
provider.
i think question is: why symfony populate wrong Email that failed validation into token and how i could prevent it?
controller:
public function meSettingsAction(Request $request)
{
$user = $this->getUser();
$userUnSubscribed = $this->getDoctrine()->getRepository('AppBundle:UserUnsubs')->findOneBy(
[
'email' => $user->getEmail(),
]
);
$form = $this->createForm(UserSettingsType::class, $user);
$form->get('subscribed')->setData(!(bool)$userUnSubscribed);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
/**
* #var $user User
*/
$user = $form->getData();
/** #var UploadedFile $avatar */
$avatar = $request->files->get('user_settings')['photo'];
$em = $this->getDoctrine()->getManager();
if ($avatar) {
$avatar_content = file_get_contents($avatar->getRealPath());
$avatarName = uniqid().'.jpg';
$oldAvatar = $user->getPhoto();
$user
->setState(User::PHOTO_STATE_UNCHECKED)
->setPhoto($avatarName);
$gearmanClient = $this->get('gearman.client');
$gearmanClient->doBackgroundDependsOnEnv(
'avatar_content_upload',
serialize(['content' => $avatar_content, 'avatarName' => $avatarName, 'oldAvatar' => $oldAvatar])
);
}
$subscribed = $form->get('subscribed')->getData();
if ((bool)$userUnSubscribed && $subscribed) {
$em->remove($userUnSubscribed);
} elseif (!(bool)$userUnSubscribed && !$subscribed) {
$userUnSubscribed = new UserUnsubs();
$userUnSubscribed->setEmail($form->get('email')->getData())->setTs(time());
$em->persist($userUnSubscribed);
}
$user->setLastTs(time());
$em = $this->getDoctrine()->getManager();
$em->persist($user);
$em->flush();
$this->get('user.manager')->refresh($user);
return $this->redirectToRoute('me');
}
return $this->render(
':user:settings.html.twig',
[
'form' => $form->createView(),
]
);
}
UPD:
it works fine if i change in OAuthProvider:
/**
* #param \Symfony\Component\Security\Core\User\UserInterface $user
*
* #return \Symfony\Component\Security\Core\User\UserInterface
*/
public function refreshUser(UserInterface $user)
{
return $this->loadUserByUsername($user->getName());
}
to:
/**
* #param \Symfony\Component\Security\Core\User\UserInterface $user
*
* #return \Symfony\Component\Security\Core\User\UserInterface
*/
public function refreshUser(UserInterface $user)
{
return $this->userManager($user->getId());
}
but it seems to be dirty hack.
Thanks.
Your user token seems to be updated by the form, even if the email constraint stop the flush.
Can you check if your form past the isValid function ?
You can maybe try to avoid it with an event listener or a validator.
With an event SUBMIT you should be able to check the email integrity, and then add a FormError to avoid the refreshUser.
This is a tricky one, thanks to the repository it was easier to isolate the problem. You are binding the user object form the authentication token to the createForm() method. After the
$form->handleRequest($request)
call the email off the token user object is updated.
I first thought to solve this by implementing the EquatableInterface.html in the User entity but this did not work, as the compared object already had the wrong email address set.
It may also be useful to implement the EquatableInterface interface, which defines a method to check if the user is equal to the current user. This interface requires an isEqualTo() method.)
Than I thought about forcing a reload of the user from the db and resetting the security token, but in the it came to my mind, that it might be sufficient to just refresh the current user object from the database in case the form fails:
$this->get('doctrine')->getManager()->refresh($this->getUser());`
In your controller, this would solve your issue.
/**
* #Route("/edit_me", name="edit")
* #Security("has_role('ROLE_USER')")
*/
public function editMyselfAction(Request $request) {
$form = $this->createForm(User::class, $this->getUser());
if ($request->isMethod(Request::METHOD_POST)) {
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$user = $form->getData();
$em = $this->getDoctrine()->getManager();
$em->persist($user);
$em->flush();
} else {
$this->get('doctrine')->getManager()->refresh($this->getUser());
}
}
return $this->render(':security:edit.html.twig',['form' => $form->createView()]);
}
Alternative solution
The issue at the Symfony repository resulted in some valuable input about Avoiding Entities in Forms and
Decoupling Your Security User which provides a more complex approach for a solution to your problem.
I have a link: mysite/productnumber_2.html. On this Site I have a formular in a twig template, the action of this form leads to .../createAssessment.
In my controller I do things like save in a DB.
My question is, how can I go back to the url mysite/productnumber_2.html?
My function in teh controller looks like this:
/**
*
* #Route(path = "/createassessment", name="create_assessment", methods = "POST")
* #param Request $request The Request object
* #return RedirectResponse
*/
public function createAssessmentAction(Request $request) {
$form = $this->createForm(new AssessmentType());
$form->handleRequest($request);
if ($form->isValid()) {
$assessment = $form->getData();
$em = $this->getDoctrine()->getManager();
$em->persist($assessment);
$em->flush();
return $this->redirect( ## WHAT DO I HAVE TO PUT HERE?? ## );
}
}
You could redirect for request referer:
return $this->redirect($request->headers->get('referer'));
Or even better for defined route:
return $this->redirect($this->generateUrl('your_route'));
where your_route is path to you mysite/productnumber_2.html page.
I have a form, which has to be passed by some other validations than unusual (about 4 fields are depending from each other). Thing is, when its failed, I redirect the user back, but then the form loses its values, I dont want it. I know it can be done with session, but there might be a "sanitier" way. Code is usual:
public function printAction()
{
if ($this->getRequest()->getMethod() == "POST")
{
$form->bindRequest($this->getRequest());
if ($form->isValid())
{
.... more validation.... Failed!
return $this->redirect($this->generateUrl("SiteHomePeltexStockStockHistory_print"));
// and this is when I lose the values.... I dont want it
}
}
}
You can use the same action for both GET and POST requests related to a form. If validation fails, just don't redirect and the same form will be redisplayed with entered values and validation error messages:
/**
* #Template
*/
public function addAction(Request $request)
{
$form = /* ... */;
if ($request->isMethod('POST')) {
$form->bind($request);
if ($form->isValid()) {
// do something and redirect
}
// the form is not valid, so do nothing and the form will be redisplayed
}
return [
'form' => $form->createView(),
];
}
You can passe your parametters to the new page when making the new redirection:
$this->redirect($this->generateUrl('SiteHomePeltexStockStockHistory_print', array('name1' => 'input1', 'name2' => 'input2', 'name3' => $input3, ....)));
or directly pass an array of post values:
$this->redirect($this->generateUrl('SiteHomePeltexStockStockHistory_print', array('values' => $values_array)));
You may want to do something like this
class FooController extends Controller
{
/**
* #Route("/new")
* #Method({"GET"})
*/
public function newAction()
{
// This view would send the form content to /create
return $this->render('YourBundle:form:create.html.twig', array('form' => $form));
}
/**
* #Route("/create")
* #Method({"POST"})
*/
public function createAction(Request $request)
{
// ... Code
if ($form->isValid()) {
if (/* Still valid */) {
// Whatever you do when validation passed
}
}
// Validation failed, just pass the form
return $this->render('YourBundle:form:create.html.twig', array('form' => $form));
}
}