FOSUserBundle - Event set variable in session when success authentication? - php

I would like to know if it is possible to insert a variable in the session if authentication succeds (using with FOSUserBundle).
It is more or less than two lines to insert.
$session = $request->getSession();
$this->$session->set('type','OneType');
Is there a very simple way to do it? I really want to do it when there is successful authentication, not anywhere.

You need to listen to the event security.interactive_login. (Docs)
Simple example, using an event subscriber:
<?php
// src/EventSubscriber/SecuritySubscriber.php
namespace App\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Http\SecurityEvents;
class SecuritySubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return [
SecurityEvents::INTERACTIVE_LOGIN => 'successfulLogin',
];
}
public function successfulLogin( InteractiveLoginEvent $event )
{
$event->getRequest()->getSession()->set('foo', 'bar');
}
}
You haven't specified version, but this should work on a default installation for Symfony 4.

Related

How to share user data in laravel dusk tests

I'm very new to Laravel Dusk (like less than 24 hours) and I'm experimenting with creating some tests but I can't wrap my head around getting past the initial test.
So I have UserCanRegisterTest.php and UserCanSeeDashboardTest.php, In UserCanRegisterTest.php I register a user, how can I access that user info in UserCanSeeDashboardTest.php without having to recreate another user? I have tried researching but I've fallen down a rabbit hole, I've looked at memory, cookies, DatabaseTransactions but nothing seems to make sense or show an example.
Is it possible for me to use the $faker->safeEmail and $password from UserCanRegisterTest.php in UserCanSeeDashboardTest.php and all other tests I make?
UserCanRegisterTest.php:
<?php
namespace Tests\Browser;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
class UserCanRegisterTest extends DuskTestCase
{
use DatabaseMigrations;
/*public function setUp()
{
parent::setUp();
$this->artisan('db:seed');
}*/
/** #test */
public function user_passes_registration_form()
{
$faker = \Faker\Factory::create();
/*$roleSeeder = new RoleTableSeeder();
$roleSeeder->run();
$permissionSeeder = new PermissionTableSeeder();
$permissionSeeder->run();*/
$this->browse(function($browser) use ($faker) {
$password = $faker->password(9);
$browser->visit('/register')
//->assertSee('Welcome Back!')
->type('company_name', $faker->company)
->type('name', $faker->name)
->type('email', $faker->safeEmail)
->type('password', $password)
->type('password_confirmation', $password)
->press('REGISTER')
->assertPathIs('/register');
});
}
}
Here is UserCanSeeDashboardTest.php (note how I'd like to use $faker->safeEmail and $password from the above test so I don't need to create new user every time).
<?php
namespace Tests\Browser;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
use App\User;
class UserCanSeeDashboardTest extends DuskTestCase
{
use DatabaseMigrations;
/*public function setUp()
{
parent::setUp();
//$this->artisan('db:seed');
}*/
/** #test */
public function test_I_can_login_successfully()
{
$this->browse(function ($browser) {
//$user->roles()->attach(1); //Attach user admin role
$browser->visit('/login')
->type('email', $faker->safeEmail)
->type('password', $password)
->press('SIGN IN')
->assertSee('Dashboard');
});
}
}
Ideally, I have a test that registers a user, then I have other tests that use that registered user's data to log in and test other parts of my app.
PHPUnit doesn't have great support for tests that depend on each other. Tests in PHPUnit should mostly be considered independent. The framework does provide the #depends annotation that you might have been able to use for you tests that depend on the registration method, but it only works for tests that are in the same class.
Also, you don't need to worry about creating multiple users because you're using the DatabaseMigrations trait that refreshes your test database for you after every test.
The way I see it, you have two options. Either you:
Move your registration code (the part starting from $browser->visit('/register')) to a new method and then call that method in both your user_passes_registration_form test and in your other tests where you want to have a registered user, or
Write a new method that you can call from your other tests that registers a user directly in your database (e.g. using User::create).
The benefit of the second option is that you'll have less HTTP calls which will result in a faster test run and only your registration test would fail (instead of all your tests) when your registration endpoint is broken.
So what I'd suggest is that you keep your registration test as is and use either a trait or inheritance to add a few methods that you can reuse to register or login a test user from other test methods.
You could create a class MyDuskTestCase that inherits from DuskTestCase and that contains a method to register a test user:
<?php
namespace Tests;
use Tests\DuskTestCase;
use App\User;
use Hash;
abstract class MyDuskTestCase extends DuskTestCase
{
private $email = 'test#example.com';
private $password = 'password';
public function setup(): void
{
parent::setUp();
// If you want to run registerTestUser for every test:
// registerTestUser();
}
public function registerTestUser()
{
User::create([
'email' => $this->email,
'name' => 'My name',
'password' => Hash::make($this->password)
]);
// assign a role, etc.
}
public function getTestUser()
{
return User::where('email', $this->email)->first();
}
}
Then you can either run the registerTestUser method in the setup method to create the test user for every test, or you can call the method from only the tests where you'll need the user. For example:
<?php
namespace Tests\Browser;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Laravel\Dusk\Browser;
use Tests\MyDuskTestCase;
class UserCanRegisterTest extends MyDuskTestCase
{
use DatabaseMigrations;
public function test_I_can_login_successfully()
{
$this->registerTestUser();
$this->browse(function ($browser) use ($user) {
$browser->visit('/login')
->type('email', $this->email)
->type('password', $this->password)
->press('SIGN IN')
->assertSee('Dashboard');
});
}
}
For logins, you can either add another method to your base test class to log the test user in, or you could use the loginAs method that Dusk provides:
$user = this->getTestUser();
$this->browse(function ($browser) {
$browser->loginAs($user)
->visit('/home');
});

Laravel: How to set globally available default route parameters

I'm trying to set a handful of default route parameters that will work globally in my application regardless of context. In the documentation for URL generation the example given is using middleware which is fine for HTTP, but won't get called during non-HTTP contexts. I also need this to work when called from the CLI.
My first idea is to have a Service Provider that calls the defaults method on boot:
<?php
namespace App\Providers;
use Illuminate\Routing\UrlGenerator;
use Illuminate\Support\ServiceProvider;
class UrlDefaults extends ServiceProvider
{
public function boot(UrlGenerator $urlGenerator): void
{
$urlGenerator->defaults([
'foo' => 'abc',
'bar' => 'xyz',
]);
}
}
But this does not work for HTTP requests:
Route::get('test', function (\Illuminate\Routing\UrlGenerator $urlGenerator) {
dump($urlGenerator->getDefaultParameters());
});
Outputs []
I believe this is because in the UrlGenerator, the setRequest method unconditionally sets the routeGenerator property to null. My Service Provider's boot method is called during the bootstrapping process, but then the request is set afterwards clobbering my defaults.
//Illuminate/Routing/UrlGenerator.php
public function setRequest(Request $request)
{
$this->request = $request;
$this->cachedRoot = null;
$this->cachedSchema = null;
$this->routeGenerator = null;
}
Dumping the UrlGenerator during boot and then again in my routes file can demonstrate this:
As you can see, the UrlGenerator instance is the same both times, but the RouteUrlGenerator on the routeGenerator property has changed.
I am unsure of a better way to set these defaults.
Not sure why this is getting attention almost a year later, but I ended up finding a solution by myself.
To add a bit more information to the original question, the purpose of this was to allow us to have the same instance of the code powering both our live and sandbox application. There's more involved to get this working, but this issue was just about URL generation for links in views. All links generated always both a subdomain and tld, so this code injects these values always.
These views are rendered both as a response to a HTTP request, e.g. in our client areas, but also as part of a non HTTP request, e.g. a scheduled task generating invoices and emailing them to clients.
Anyway, the solution:
For non HTTP contexts, a service provider can set the defaults:
<?php namespace App\Providers;
use App\Support\UrlDefaults;
use Illuminate\Routing\UrlGenerator;
use Illuminate\Support\ServiceProvider;
class UrlDefaultsServiceProvider extends ServiceProvider
{
public function boot(UrlGenerator $urlGenerator): void
{
$urlGenerator->defaults(UrlDefaults::getDefaults());
}
}
Since the there's no routing going on to cause the problem I asked originally, this just works.
For HTTP contexts, the RouteMatched event is listened for and the defaults injected then:
<?php namespace App\Listeners;
use App\Support\UrlDefaults;
use Illuminate\Routing\Router;
use Illuminate\Routing\UrlGenerator;
/**
* Class SetUrlDefaults
*
* This class listeners for the RouteMatched event, and when it fires, injects the route paramaters (subdomain, tld,
* etc) into the defaults of the UrlGenerator
*
* #package App\Listeners
*/
class SetUrlDefaults
{
private $urlGenerator;
private $router;
public function __construct(UrlGenerator $urlGenerator, Router $router)
{
$this->urlGenerator = $urlGenerator;
$this->router = $router;
}
public function handle(): void
{
$paramaters = array_merge(UrlDefaults::getDefaults(), $this->router->current()->parameters);
$this->urlGenerator->defaults($paramaters);
}
}
UrlDefaults is just a simple class that returns an array:
<?php namespace App\Support;
class UrlDefaults
{
public static function getDefaults(): array
{
return [
'tld' => config('app.url.tld'),
'api' => config('app.url.api'),
'foo' => config('app.url.foo'),
'bar' => config('app.url.bar'),
];
}
}
So digging into the source for routing classes a bit more, there’s a defaults() method on the UrlGenerator class, but it’s not a singleton, so any defaults you set in a service provider aren’t persisted.
I seem to have got it working by setting the defaults in some middleware:
Route::domain('{domain}')->middleware('route.domain')->group(function () {
//
});
namespace App\Http\Middleware;
use Illuminate\Contracts\Routing\UrlGenerator;
class SetRouteDomain
{
private $url;
public function __construct(UrlGenerator $url)
{
$this->url = $url;
}
public function handle($request, Closure $next)
{
$this->url->defaults([
'domain' => $request->getHost(),
]);
return $next($request);
}
}

Symfony 3 - Can't pass token_storage to from subscriber

I want to get current logged user in form event but for some reason I can't get it to work.
I used services to inject token_storage and create constructor method to fetch token storage instance but I got error right at constructor:
Type error: Argument 1 passed to AdminBundle\Form\EventListener\CompanyFieldSubscriber::__construct() must implement interface Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface, none given
I am not sure what is the problem and how to fix it. Does someone knows where is the problem?
EDIT 1:
I think that I found out where is the problem but I can't find "good" solution. I call this event in form type in this way:
->addEventSubscriber(new CompanyFieldSubscriber());
Problem is that I am not using 'service/dependency injection' to create event and I am sending nothing to constructor. That's why I have this error (not 100% sure to be hones).
Since I have around 20-30 forms and new forms will come in time I need to create service for each form that requires user (or token_storage) instance and as a argument call token_storage or this event subscriber as a argument of service.
I know that it will work if I create each form as a service and pass required data as arguments but is there way to process this "automatically" without creating new service for every form that needs to have some user data interaction in form events?
EDIT 2:
As suggested I tried to change event subscriber constructor but I got same error with different class name.
New code:
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
public function __construct(TokenStorage $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}
New error:
Type error: Argument 1 passed to AdminBundle\Form\EventListener\CompanyFieldSubscriber::__construct() must be an instance of Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage, none given
This is code I am using:
Services:
admin.form.event_listener.company:
class: AdminBundle\Form\EventListener\CompanyFieldSubscriber
arguments: ['#security.token_storage']
tags:
- { name: form.event_listener }
Event Listener:
namespace AdminBundle\Form\EventListener;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
class CompanyFieldSubscriber implements EventSubscriberInterface
{
private $tokenStorage;
private $user;
public function __construct(TokenStorageInterface $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
$this->user = $this->tokenStorage->getToken()->getUser();
}
public static function getSubscribedEvents()
{
return [
FormEvents::PRE_SET_DATA => 'preSetData',
FormEvents::PRE_SUBMIT => 'preSubmitData',
];
}
public function preSetData(FormEvent $event)
{
$form = $event->getForm();
if (in_array("ROLE_SUPER_ADMIN", $this->user->getRoles())) {
$form->add('company', EntityType::class, [
'class' => 'AppBundle:Company',
'choice_label' => 'name'
]);
}
}
public function preSubmitData(FormEvent $event)
{
$form = $event->getForm();
$bus = $form->getData();
if (!in_array("ROLE_SUPER_ADMIN", $this->user->getRoles())) {
$bus->setCompany($this->user->getCompany());
}
}
}
You call subscriber in wrong way when you use:
new CompanyFieldSubscriber()
You do not pass TokenStorageInterface to subscriber constructor. Call it as a service, if it is in controller then:
->addEventSubscriber($this->get('admin.form.event_listener.company'));
if it is in form than pass from controller to form
$this->get('admin.form.event_listener.company')
as option and then use it in form
for Symfony >= 4
use Symfony\Component\Security\Core\Security;
class ExampleService
{
private $security;
public function __construct(Security $security)
{
$this->security = $security;
}
public function someMethod()
{
$user = $this->security->getUser();
}
}
See doc: https://symfony.com/doc/current/security.html#fetching-the-user-from-a-service

Symfony2 workaround when devices have cookies disabled

For a project I am required to have a persistent session for a visitor.
A couple of years ago I faced the issue with an Apple update temporary rendering all iPhones unable to set PHPSESSID cookies.
I created a fall back method which checked for the SESSION ID in the URL and use that to persist the session between requests. I am aware of the fact this can be enabled in php.ini using the session.use_trans_sid.
Point is I do not want this to happen always. When possible I prefer the cookie method.
Is there a way within Symfony to add this logic to the route methods adding the session identifier?
Can anyone help me to explain where to extend the twig "path" method to add the logic to optionally append the session id to all URL's generated by that method.
UPDATE
Let me post an update on my progress and perhaps someone can help me. I managed to find how to extend the UrlGenerator with my own code by replacing the generator_base_class in a parameter.
Now I have the following issue.
I wish to use a session to do some logic. I however can not reach this core component as a service. I already tried makign a compilerPass for both the UrlGenerator and an extended Router class to be able to make a dependency injection in one of these classes.
However until now it sadly failed.
What would be the best partice to get the Session component within the UrlGenerator class?
I was able to create my solution thanks to this post:
Override router and add parameter to specific routes (before path/url used)
In the end this is the code I came up with.
In my service.xml
<parameters>
<parameter key="router.class">Acme\CoreBundle\Component\Routing\Router</parameter>
<parameter key="router.options.generator_base_class">Acme\CoreBundle\Component\Routing\Generator\UrlGenerator</parameter>
</parameters>
Extending Symfony's core router to make in ContainerAware and force that container to the UrlGenerator.
namespace Acme\CoreBundle\Component\Routing;
use Symfony\Bundle\FrameworkBundle\Routing\Router as BaseRouter;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Routing\RequestContext;
class Router extends BaseRouter implements ContainerAwareInterface
{
private $container;
public function __construct(ContainerInterface $container, $resource, array $options = array(), RequestContext $context = null)
{
parent::__construct($container, $resource, $options, $context);
$this->setContainer($container);
}
public function getGenerator()
{
$generator = parent::getGenerator();
$generator->setContainer($this->container);
return $generator;
}
public function setContainer(ContainerInterface $container = null)
{
$this->container = $container;
}
}
Extending the UrlGenerator class.
namespace Acme\CoreBundle\Component\Routing\Generator;
use Symfony\Component\Routing\Generator\UrlGenerator as BaseUrlGenerator;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* UrlGenerator generates URL based on a set of routes, this class extends the basics from Symfony.
*/
class UrlGenerator extends BaseUrlGenerator implements ContainerAwareInterface
{
private $container;
public function setContainer(ContainerInterface $container = null)
{
$this->container = $container;
}
protected function doGenerate($variables, $defaults, $requirements, $tokens, $parameters, $name, $referenceType, $hostTokens, array $requiredSchemes = array())
{
/** #var \Symfony\Component\HttpFoundation\Session\Session $session */
$session = $this->container->get('session');
if (true !== $session->get('acceptCookies')) {
$parameters[$session->getName()] = $session->getId();
}
return parent::doGenerate($variables, $defaults, $requirements, $tokens, $parameters, $name, $referenceType, $hostTokens, $requiredSchemes);
}
}
In the end this results in the session name and id being appended to the generated URL when the session value acceptCookies is not equal to true.

symfony 2 set locale based on user preferences stored in DB

I am trying to set the locale based on the current user's preferences which are stored in the DB.
Our User class therefore has a getPreferredLanguage which returns a locale identify ('en', 'fr_FR', etc.).
I've considered the following approach:
register a "locale" listener service that subscribes to the KernelEvents::REQUEST event.
this service has access to the security context (via its constructor)
this service's onKernelRequest method attempts to get the user from the security context, get the user's preferred locale, and set it as the request's locale.
Unfortunately, this doesn't work. When the "locale" listener service's onRequestEvent method is invoked, the security context does not have a token. It seems that the context listener is invoked at a very late stage (with a priority of 0), and it is impossible to tell my "locale" listener to run before the security context.
Does anyone know how to fix this approach, or suggest another one?
You may be interested in the locale listener, which I posted in this answer: Symfony2 locale detection: not considering _locale in session
Edit: If a user changes his language in the profile, it's no problem. You can hook into profile edit success event if you're are using FOSUserBundle (master). Otherwise in your profile controller, if you're using a self made system. Here is a example for FOSUserBundle:
<?php
namespace Acme\UserBundle\EventListener;
use FOS\UserBundle\Event\FormEvent;
use FOS\UserBundle\FOSUserEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class ChangeLanguageListener implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return array(
FOSUserEvents::PROFILE_EDIT_SUCCESS => 'onProfileEditSuccess',
);
}
public function onProfileEditSuccess(FormEvent $event)
{
$request = $event->getRequest();
$session = $request->getSession();
$form = $event->getForm();
$user = $form->getData();
$lang = $user->getLanguage();
$session->set('_locale', $lang);
$request->setLocale($lang);
}
}
and in the services.yml
services:
acme.change_language:
class: Acme\UserBundle\EventListener\ChangeLanguageListener
tags:
- { name: kernel.event_subscriber }
for multiple sessions in multiple browser is no problem, as every new session requires a new login. Hmm, ok, not after changing the language, as only the current session would be updated. But you can modify the LanguageListener to support this.
And the case if an admin changes the language should be insignificant.
If you reach this answer through Google, I am currently using this solution.
<?php
namespace App\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Event\ControllerEvent;
use Symfony\Component\Security\Core\Security;
use Symfony\Contracts\Translation\TranslatorInterface;
class SetLocaleEventSubscriber implements EventSubscriberInterface
{
private Security $security;
private TranslatorInterface $translator;
public function __construct(Security $security, TranslatorInterface $translator)
{
$this->security = $security;
$this->translator = $translator;
}
public static function getSubscribedEvents()
{
return [
KernelEvents::CONTROLLER => [
['setLocale', 1]
]
];
}
public function setLocale(ControllerEvent $event)
{
if (!$event->isMasterRequest()) {
return;
}
$request = $event->getRequest();
/**
* #var \App\Entit\User
*/
$user = $this->security->getUser();
if ($user) {;
$request->setLocale($user->getLocale());
$this->translator->setLocale($user->getLocale());
}
}
}
In order to achieve this, you need to setup an event subscriber on the Kernel::REQUEST event with a higher priority than the default Locale listener as indicated in the documentation
At this time, you will unfortunately not be able to access to the current logged in user because this is something set in another Symfony event triggered after the Locale listener.
However, you can access to the session.
The solution is to save the user's locale in the session just after a successful login, and then set the locale in the request from the session.
// src/EventSubscriber/UserLocaleSubscriber.php
namespace App\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\Security\Http\SecurityEvents;
/**
* Stores the locale of the user in the session after the
* login. This can be used by the LocaleSubscriber afterwards.
*/
class UserLocaleSubscriber implements EventSubscriberInterface
{
private $requestStack;
public function __construct(RequestStack $requestStack)
{
$this->requestStack = $requestStack;
}
public function onInteractiveLogin(InteractiveLoginEvent $event)
{
$user = $event->getAuthenticationToken()->getUser();
if (null !== $user->getLocale()) {
$this->requestStack->getSession()->set('_locale', $user->getLocale());
}
}
public static function getSubscribedEvents()
{
return [
SecurityEvents::INTERACTIVE_LOGIN => 'onInteractiveLogin',
];
}
}
// src/EventSubscriber/LocaleSubscriber.php
namespace App\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
class LocaleSubscriber implements EventSubscriberInterface
{
public function onKernelRequest(RequestEvent $event)
{
$request = $event->getRequest();
if ($request->getSession()) {
// Set user's locale from session
if ($locale = $request->getSession()->get('_locale')) {
$request->setLocale($locale);
}
}
}
public static function getSubscribedEvents()
{
return [
// must be registered before (i.e. with a higher priority than) the default Locale listener
KernelEvents::REQUEST => [['onKernelRequest', 20]],
];
}
}

Categories