FosUserBundle controller override - symfony

[SETTINGS]
Symfony 3.4
FosUserBundle 2.0
RESTRICTION: Avoid bundle inheritance (This is also bundle inheritance)
[PROBLEM]
While reading the Symfony doc about how to override any part if a bundle,
I met those lines:
If the controller is a service, see the next section on how to override it. Otherwise, define a new route + controller with the same path associated to the controller you want to override (and make sure that the new route is loaded before the bundle one).
And somehow felt overjoyed seeing how the doc was still as incomplete as ever on some of the most important sections... Right, this part got no code example, can't even be sure of what to do.
Would someone be kind enough to give me an example on how to override the FosUserBundle? Just one section like the login part will be enough. As the same logic will apply for the other sections.
Also, as a side questions:
Is it worth using FosUserBundle?
Is there a bundle easier to use than FosUserBundle?
Wouldn't it be more worth and faster to make my own logic to handle login?

What I understand : simply create your controller and then add a route for it in your configuration with the same path as the one you want to override, making sure it's loaded before.
For example, to override the login action:
// AppBundle\Controller\UserController.php
/**
* #route("/login", name="login_override")
* #param Request $request
* #return Response
*/
public function loginAction(Request $request)
{
/** #var $session Session */
$session = $request->getSession();
$authErrorKey = Security::AUTHENTICATION_ERROR;
$lastUsernameKey = Security::LAST_USERNAME;
// get the error if any (works with forward and redirect -- see below)
if ($request->attributes->has($authErrorKey)) {
$error = $request->attributes->get($authErrorKey);
} elseif (null !== $session && $session->has($authErrorKey)) {
$error = $session->get($authErrorKey);
$session->remove($authErrorKey);
} else {
$error = null;
}
if (!$error instanceof AuthenticationException) {
$error = null; // The value does not come from the security component.
}
// last username entered by the user
$lastUsername = (null === $session) ? '' : $session->get($lastUsernameKey);
$tokenManager = $this->container->get('security.csrf.token_manager');
$csrfToken = $tokenManager
? $tokenManager->getToken('authenticate')->getValue()
: null;
return $this->render('#FOSUser/Security/login.html.twig', array(
'last_username' => $lastUsername,
'error' => $error,
'csrf_token' => $csrfToken,
));
}
#app\config\routing.yml
app:
resource: '#AppBundle/Controller/'
type: annotation
fos_user:
resource: "#FOSUserBundle/Resources/config/routing/all.xml"

Related

Symfony - Edit User Entity without changing password

I'm running Symfony 3.4.14 and I developed my own User Bundle, I have a very bad experience with FOS then I don't want to use anymore. My goal is to let the admin users create/edit/remove users, I mean other users account.
I made :
the User entity
the Login form
the Registration form
.. and I'm stuck with the Update form. I want to let the admins edit a user without editing the password, but to give them the opportunity to do it if needed. Below is my EditUserAction in controller :
<?php
/**
* #Route("/admin/users/edit/{id}", requirements={"id" = "\d+"}, name="admin_users_edit")
* #Template("#Core/admin/users_edit.html.twig")
* #Security("has_role('ROLE_ADMIN')")
*/
public function EditUserAction($id, Request $request, UserPasswordEncoderInterface $passwordEncoder)
{
$user = $this->getDoctrine()->getRepository('CoreBundle:User')->findOneBy([ 'id'=>$id, 'deleted' => 0 ]);
if ( $user )
{
$old_password = $user->getPassword();
$form = $this->createForm(UserType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid())
{
// If admin changed the user password
if ( $user->getPlainPassword() )
{
$password = $passwordEncoder->encodePassword($user, $user->getPlainPassword());
$user->setPassword($password);
}
// If admin didn't change the user password, we persist the old one
else
{
$user->setPassword($old_password);
}
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($user);
$entityManager->flush();
}
return array('form' => $form->createView());
}
return $this->redirectToRoute('admin_users');
}
Case 1 (when admin choose to change the user password) works well, but the other case (when admin don't want to change the password) fails. They are no way to let the 2 password inputs empty. I can't get rid of this validation error in the debug toolbar :
Path: data.plainPassword
Message: This value should not be blank
In order to avoid this error, as you can see in my controller above, I try to keep the old one (may not be a best practice, I know).
In your form / entity (where you defined validations) you should write either your own constraint (documentation) or validation callback (documentation). In there you can check - If value is null, don't validate, if not null run your validations.
You need to have two separate methods for both.
public function changePasswordAction(Request $request)
{
// your code for changing password.
}
/**
* #Route("/admin/users/edit/{id}", requirements={"id" = "\d+"}, name="admin_users_edit")
* #Template("#Core/admin/users_edit.html.twig")
* #Security("has_role('ROLE_ADMIN')")
*/
public function editUserAction(User $user = null, Request $request, UserPasswordEncoderInterface $passwordEncoder)
{
//you can directly give your User Entity reference in parameters and you dont need to write an extra query to find user.
if ( $user === null){
//return user not found
}
else if($user->isDeleted() === false)
{
$form = $this->createForm(UserType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid())
{
//your code
}
// your code
}
return $this->redirectToRoute('admin_users');
}

FOSOAuthServerBundle vs Symfony3 Security howto

I had implemented FOSOAuthServerBundle with Symfony2.8 successfully and it worked.
When I tried to work it with Symfony3.2 I had error:
Attempted to load class "SecurityContext" from namespace "Symfony\Component\Security\Core".
Did you forget a "use" statement for another namespace?
So I googled and now know that SecurityContext doesn't exists in Symofny 3.2 anymore. But in FOSOAuthServerBundle official documentation "A Note About Security" still exists function loginAction() only compactible with symfony2.
Question is:
- can I use this bundle with Symfony 3.2 ?
- If yes, is there any documentation how to do it, or better any example ?
thank you very much for answers
Don't know the FOSOAuthServerBundle in detail. But I guess the example in the documentation of the bundle at a_note_about_security is just outdated.
The security.context service was deprecated since symfony 2.6. Here you can find a description of the changes: symfony.com/blog/new-in-symfony-2-6-security-component-improvements
You could try to replace \Symfony\Component\Security\Core\SecurityContext with \Symfony\Component\Security\Core\Security
<?php
// src/Acme/SecurityBundle/Controller/SecurityController.php
namespace Acme\SecurityBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Security\Core\Security;
class SecurityController extends Controller
{
public function loginAction()
{
$request = $this->getRequest();
$session = $request->getSession();
// get the login error if there is one
if ($request->attributes->has(Security::AUTHENTICATION_ERROR)) {
$error = $request->attributes->get(Security::AUTHENTICATION_ERROR);
} else {
$error = $session->get(Security::AUTHENTICATION_ERROR);
$session->remove(Security::AUTHENTICATION_ERROR);
}
// Add the following lines
if ($session->has('_security.target_path')) {
if (false !== strpos($session->get('_security.target_path'), $this->generateUrl('fos_oauth_server_authorize'))) {
$session->set('_fos_oauth_server.ensure_logout', true);
}
}
return $this->render('AcmeSecurityBundle:Security:login.html.twig', array(
// last username entered by the user
'last_username' => $session->get(Security::LAST_USERNAME),
'error' => $error,
));
}
}
I figured it out.
Maybe it help somebody.
public function loginAction(Request $request) {
$authenticationUtils = $this->get('security.authentication_utils');
$error = $authenticationUtils->getLastAuthenticationError();
$lastUsername = $authenticationUtils->getLastUsername();
return $this->render('AppBundle:Security:login.html.twig', array(
'last_username' => $lastUsername
, 'error' => $error
));
}

Symfony2 fosuserbundle deal with/catch unique constraint violation

I'm using symfony 2.8 and FOSUserBundle. I want to allow admins to edit users' usernames and emails. If the new username or email is already taken then the database gives an error
SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry
which is good, but I don't know how to communicate that back to the admin who tried to change it to tell them what went wrong (the production version of the app will just give an error 500). What I want to do is show an error message of some kind (preferable like the one FOSUserBundle has in its forms) to say the username (or email) is taken.
The relevant portions of the form is built here:
$userManager = $this->get('fos_user.user_manager');
$user = $userManager->findUserBy(array('id' => $id));
$form = $this->createFormBuilder()
->add('username', TextType::class, array(
'label' => 'Username',
'data' => $user->getUsername(),
))
->add('email', EmailType::class, array(
'label' => 'Email',
'data' => $user->getEmail(),
))
->getForm();
and the database is handled here:
if ($form->isSubmitted() and $form->isValid()) {
// set new username if different
$newUsername = $form['username']->getData();
if ($user->getUsername() !== $newUsername) {
$user->setUsername($newUsername);
}
// set new email if different
$newEmail = $form['email']->getData();
if ($user->getEmail() !== $newEmail) {
$user->setEmail($newEmail);
}
$userManager->updateUser($user);
}
I have tried a number of things, like also setting username_canonical and email_canonical, or adding #UniqueEntity in my User.php class, but they haven't helped (which makes sense since the error is correct - I just can't translate it into a useful message).
If you doesn't want override anymore for make some validation, you need to implement an EventListener that catch the exceptions of your need by listening on the onKernelResponse event.
DBALExceptionResponseListener.php
// src/AcmeBundle/EventListner/DBALExceptionResponseListener.php
<?php
namespace AcmeBundle\EventListener;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Doctrine\DBAL\DBALException;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\HttpFoundation\Router\RouterInterface;
class DBALExceptionResponseListener
{
public function __construct(SessionInterface $session, RouterInterface $router)
{
$this->session = $session;
$this->router = $router;
}
/**
* #param GetResponseForExceptionEvent $event
*/
public function onKernelResponse(GetResponseForExceptionEvent $event)
{
$request = $event->getRequest();
$exception = $event->getException();
$message = $exception->getMessage();
// Maybe some checks on the route
if ($request->get('_route') !== 'your_route' || $request->headers->get('referer') !== 'your_referer') {
return;
}
// Listen only on the expected exception
if (!$exception instanceof DBALException) {
return;
}
// You can make some checks on the message to return a different response depending on the MySQL error given.
if (strpos($message, 'Integrity constraint violation')) {
// Add your user-friendly error message
$this->session->getFlashBag()->add('error', 'SQL Error: '.$message);
}
// Create your custom response to avoid the error page.
$response = new RedirectResponse($this->router->generate('your_route'));
// Update the Event Response with yours
$event->setResponse($response);
}
}
services.yml
# app/config/services.yml
services:
acme.kernel.listener.dbal_exception_response_listener:
class: AcmeBundle\EventListener\DBALExceptionResponseListener
tags:
- {name: kernel.event_listener, event: kernel.exception, method: onKernelResponse}
arguments:
session: "#session"
router: "#router"
By looking more at the Exception::$message, you can easily find which property causes the problem.
The most common message contains something like :
... column 'propertyname' cannot be null ...

FOSUserBundle - PHPUnit - Mock a user

I am using Symfony with the FOSUserBundle and now I like to test some things like:
Doctrine lifecycle
Controller behind firewall
For those tests I need to be a specific user or at least in a user group.
How do I mock a user session so that ...
The lifecycle field like "createdAt" will use the logged in user
The Controller act like some mocked user is logged in
Example:
class FooTest extends ... {
function setUp() {
$user = $this->getMock('User', ['getId', 'getName']);
$someWhereGlobal->user = $user;
// after this you should be logged in as a mocked user
// all operations should run using this user.
}
}
You can do this with LiipFunctionalTestBundle. Once you have installed and configured the Bundle, creating and user and log in in tests is easy.
Create a fixture for your user
This creates a user which will be loaded during tests:
<?php
// Filename: DataFixtures/ORM/LoadUserData.php
namespace Acme\MyBundle\DataFixtures\ORM;
use Doctrine\Common\DataFixtures\AbstractFixture;
use Doctrine\Common\DataFixtures\FixtureInterface;
use Doctrine\Common\Persistence\ObjectManager;
use Acme\MyBundle\Entity\User;
class LoadUserData extends AbstractFixture implements FixtureInterface
{
public function load(ObjectManager $manager)
{
$user = new User();
$user
->setId(1)
->setName('foo bar')
->setEmail('foo#bar.com')
->setPassword('12341234')
->setAlgorithm('plaintext')
->setEnabled(true)
->setConfirmationToken(null)
;
$manager->persist($user);
$manager->flush();
// Create a reference for this user.
$this->addReference('user', $user);
}
}
If you want to use groups of users, you can see the official documentation.
Log in as this user in your test
As explained in LiipFunctionalTestBundle's documentation, here is how to load the user in the database and log in as this user:
/**
* Log in as the user defined in the Data Fixture.
*/
public function testWithUserLoggedIn()
{
$fixtures = $this->loadFixtures(array(
'Acme\MyBundle\DataFixtures\ORM\LoadUserData',
));
$repository = $fixtures->getReferenceRepository();
// Get the user from its reference.
$user = $repository->getReference('user')
// You can perform operations on this user.
// ...
// And perform functional tests:
// Create a new Client which will be logged in.
$this->loginAs($user, 'YOUR_FIREWALL_NAME');
$this->client = static::makeClient();
// The user is logged in: do whatever you want.
$path = '/';
$crawler = $this->client->request('GET', $path);
}
What I would do in this case is to create a CustomWebTestCase which extends the Symfony WebTestCase. In the class I would create a method which does the authentication for me.
Here is an example code:
namespace Company\MyBundle\Classes;
use Symfony\Bundle\FrameworkBundle\Client;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\BrowserKit\Cookie;
use Symfony\Component\Security\Core\Role\Role;
use Symfony\Component\Security\Core\User\User;
abstract class CustomWebTestCase extends WebTestCase
{
/**
* #param array|null $roles
* #return \Symfony\Bundle\FrameworkBundle\Client
*/
protected static function createAuthenticatedClient(array $roles = null) {
// Assign default user roles if no roles have been passed.
if($roles == null) {
$role = new Role('ROLE_SUPER_ADMIN');
$roles = array($role);
} else {
$tmpRoles = array();
foreach($roles as $role)
{
$role = new Role($role, $role);
$tmpRoles[] = $role;
}
$roles = $tmpRoles;
}
$user = new User('test_super_admin', 'passwd', $roles);
return self::createAuthentication(static::createClient(), $user);
}
private static function createAuthentication(Client $client, User $user) {
// Read below regarding config_test.yml!
$session = $client->getContainer()->get('session');
// Authenticate
$firewall = 'user_area'; // This MUST MATCH the name in your security.firewalls.->user_area<-
$token = new UsernamePasswordToken($user, null, $firewall, $user->getRoles());
$session->set('_security_'.$firewall, serialize($token));
$session->save();
// Save authentication
$cookie = new Cookie($session->getName(), $session->getId());
$client->getCookieJar()->set($cookie);
return $client;
}
}
The code above will directly create a valid user session and will skip the firewall entirely. Therefore you can create whatever $user you want and it will still be valid. The important part of the code is located in the method createAuthentication. This is what does the authentication magic.
One more thing worth mentioning - make sure you have set framework.session.storage_id to session.storage.mock_file in your config_test.yml so that Symfony will automatically mock sessions instead of you having to deal with that in each test case:
framework:
session:
storage_id: session.storage.mock_file
Now in your test case you would simply extend MyWebTestCase and call the createAuthenticatedClient() method:
class MyTest extends CustomWebTestCase {
public function testSomething() {
//Create authoried and unauthorized clients.
$authenticatedClient = self::createAuthenticatedClient(array("ROLE_SUPER_ADMIN"));
$unauthorizedClient = self::createAuthenticatedClient(array("ROLE_INSUFFICIENT_PERMISSIONS"));
// Check if the page behaves properly when the user doesn't have necessary role(s).
$unauthorizedClient->request('GET', '/secured-page');
$response = $unauthorizedClient->getResponse();
$this->assertFalse($response->isSuccessful());
$this->assertEquals(403, $response->getStatusCode(), "This request should have failed!");
// Check if the page behaves properly when the user HAS the necessary role(s)
$authenticatedClient->request('GET', '/secured-page');
$response = $authenticatedClient->getResponse();
$this->assertTrue($response->isSuccessful());
$this->assertEquals(200, $response->getStatusCode(), "This request should be working!");
}
}
You can see an example in the Symfony official documentation as well.
You can easily do that with LiipFunctionalTestBundle which authorize you lot of shortcut for create Unit Test.
If already you have a form user for create or edit you can use this for your test unit workflow user in your application :
use the makeClient method for logging test
$credentials = array(
'username' => 'a valid username',
'password' => 'a valid password'
);
$client = static::makeClient($credentials);
use your form for test your creation
$crawler = $client->request('GET', '/profile');
$form = $crawler->selectButton('adding')->form();
$form['fos_user_profile_form[firstName]'] = 'Toto';
$form['fos_user_profile_form[lastName]'] = 'Tata';
$form['fos_user_profile_form[username]'] = 'dfgdgdgdgf';
$form['fos_user_profile_form[email]'] = 'testfgdf#grgreger.fr';
$form['fos_user_profile_form[current_password]'] = 'gfgfgdgpk5dfgddf';
testing "createdAt" with just call findOneBy in repository user like this
$user = $this->getObjectManager()
->getRepository('AcmeSecurityBundle:User')
->findOneBy(array('username' => 'testCreateUserUsername'));
$this->assertTrue($user->getCreatedAt() == now());

Symfony2 FOSUser Login After clear cache

I have a problem with my FosUser login in this days i try to render the controller of my login html page in navbar and the login work well..
Now i put my site on the server, i clear the cache and after login the page refreshes without log in the site..
This is the controller
namespace FOS\UserBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\SecurityContextInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
class SecurityController extends Controller
{
public function loginAction(Request $request)
{
/** #var $session \Symfony\Component\HttpFoundation\Session\Session */
$session = $request->getSession();
// get the error if any (works with forward and redirect -- see below)
if ($request->attributes->has(SecurityContextInterface::AUTHENTICATION_ERROR)) {
$error = $request->attributes->get(SecurityContextInterface::AUTHENTICATION_ERROR);
} elseif (null !== $session && $session->has(SecurityContextInterface::AUTHENTICATION_ERROR)) {
$error = $session->get(SecurityContextInterface::AUTHENTICATION_ERROR);
$session->remove(SecurityContextInterface::AUTHENTICATION_ERROR);
} else {
$error = null;
}
if (!$error instanceof AuthenticationException) {
$error = null; // The value does not come from the security component.
}
// last username entered by the user
$lastUsername = (null === $session) ? '' : $session->get(SecurityContextInterface::LAST_USERNAME);
$csrfToken = $this->has('form.csrf_provider')
? $this->get('form.csrf_provider')->generateCsrfToken('authenticate')
: null;
return $this->renderLogin(array(
'last_username' => $lastUsername,
'error' => $error,
'csrf_token' => $csrfToken,
));
}
/**
* Renders the login template with the given parameters. Overwrite this function in
* an extended controller to provide additional data for the login template.
*
* #param array $data
*
* #return \Symfony\Component\HttpFoundation\Response
*/
protected function renderLogin(array $data)
{
return $this->render('FOSUserBundle:Security:login.html.twig', $data);
}
public function checkAction()
{
throw new \RuntimeException('You must configure the check path to be handled by the firewall using form_login in your security firewall configuration.');
}
public function logoutAction()
{
throw new \RuntimeException('You must activate the logout in your security firewall configuration.');
}
}
How can i do??
Did you do a:
app/console cache:clear --env=prod
or a:
app/console cache:clear
You should use the first one for your production environment
https://reformatcode.com/browse

Resources