I'm migrating quite a large community to symfony2. The current user table contains a lot of users with non-alphanumeric chars in the username. In the new version I only allow [a-zA-Z0-9-] for benefits like semantic URLs for each user.
Is it possible to catch users who log in with email/pass and have no username set? I would like them to redirect to a page where they will be able to re-pick a username. The tricky part: they should not be able to touch anything on the site unless they have a correct username.
I thought about a event, from the fosuserbundle but I couldn't find a suitable one.
You could use events. See an example here: http://symfony.com/doc/2.0/cookbook/event_dispatcher/before_after_filters.html
Of course the action changing the username should be ignored by the event listener. Just like login and other anonymous actions.
You can return any response, including a redirect, by setting response on an event.
Just an idea. How about the AOP paradigm (JMSAopBundle)? Define a pointcut for you controllers (except for the login one):
class PrivateEntityInformationPointcut implements PointcutInterface
{
public function matchesClass(\ReflectionClass $class)
{
return $class->isSubclassOf('Your\Controller\Superclass')
&& $class->name !== 'Your\Controller\Access';
}
public function matchesMethod(\ReflectionMethod $method)
{
return true; // Any method
}
}
Then the interceptor should redirect to the page for setting the username:
class DenyEntityAccessInterceptor implements MethodInterceptorInterface
{
private $securityContext;
private $logger;
/**
* #DI\InjectParams({
* "securityContext" = #DI\Inject("security.context"),
* "logger" = #DI\Inject("logger"),
* })
*/
public function __construct(SecurityContext $securityContext,
Logger $logger)
{
$this->securityContext = $securityContext;
$this->logger = $logger;
}
public function intercept(MethodInvocation $invocation)
{
// Check username, redirect using the router, log what's happening
// It's OK
return $invocation->proceed();
}
}
Related
I have an entity BlogPost with a status property. This status property depends on an external API call which is handled via the doctrine postLoad event. All other properties are stored in the local database.
public function postLoad(BlogPost $post)
{
$this->postHandler->calculateStatus($post);
}
The problem is, in some cases i don't want to calculate the status at all. For example if i want to get only the description of all blogposts.
With the code above, all blog entities being loaded will trigger the postLoad event even if i just want to have values from a local database. That is very expensive and not acceptable.
So for example in my repository class i want to get all BlogPosts having a website without invoking the postLoad event.
public function findBlogPosts()
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->select('bp')
->from('AppBundle:BlogPosts', 'bp')
->innerJoin('bp.website', 'w');
return $qb->getQuery()->getResult();
}
Is there a way to say "Yes, load the BlogPost collection, but do not fire event!" ???
Any other approaches? Custom event?
Thanks
Why don't just move this logic outside the post entity and event listener? If you know when you need to calculate the status you can do it explicitly.
For example
$post = $this->entityManager->find(BlogPost::class, $postId);
$status = $this->postHandler->calculateStatus($post);
The other approach I could suggest is not good but works. You could use lazy calculation and instead of calling $this->postHandler->calculateStatus($this) in postLoad event listener you could inject postHandler service into entity and perform the calculation in the moment you actually need it.
For example if you need calculation when calling $blogPost->getStatus() method, you could do it this way:
interface PostHandlerAwareInterface
{
public function setPostHandler(PostHandlerInterface $postHandler): void;
}
class EntityServiceInjectorEventSubscriber implements EventSubscriber
{
/** #var PostHandlerInterface */
private $postHandler;
public function postLoad($entity): void
{
$this->injectServices($entity);
}
public function postPersist($entity): void
{
$this->injectServices($entity);
}
private function injectServices($entity): void
{
if ($entity instanceof PostHandlerAwareInterface) {
$entity->setPostHandler($this->postHandler);
}
}
}
class BlogPost extends PostHandlerAwareInterface
{
/** #var PostHandlerInterface */
private $postHandler;
private $status;
public function setPostHandler(PostHandlerInterface $postHandler): void
{
$this->postHandler = $postHandler;
}
public function getStatus()
{
if (null === $this->status) {
$this->postHandler->calculateStatus($this);
}
return $this->status;
}
}
If you don't like this idea you still could manage it via (BUT I STRONGLY DO NOT RECOMMEND DO THIS DIRTY HACK) setting the flag to your entity event listener.
You could inject your entity event listener to the code and set flag before fetching data:
class BlogPostCalculateStatusListener
{
/** #var bool */
private $calculationEnabled = true;
public function suspendCalculation(): void
{
$this->calculationEnabled = false;
}
public function resumeCalculation(): void
{
$this->calculationEnabled = true;
}
public function postLoad(BlogPost $post): void
{
if ($this->calculationEnabled) {
$this->postHandler->calculateStatus($post);
}
}
}
$this->calculateStatusListener->suspendCalculation();
$blogPosts = $blogPostRepository->findBlogPosts();
$this->calculateStatusListener->resumeCalculation();
Hope this helps.
PS. If you want to get only the descriptions of all blog posts you can do this way:
class BlogPostRepository
{
public function findBlogPosts()
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->select('bp.description')
->from('AppBundle:BlogPosts', 'bp')
->innerJoin('bp.website', 'w');
return $qb->getQuery()->getArrayResult();
}
}
getArrayResult does not invoke lifecycle callbacks.
Since i haven't found a real similar use case on the internet, i'll go for the following solution which seems the easiest and most acceptable cleanest to me. Maybe someone else could find this useful.
Implement a TransientLoadable Interface
interface TransientLoadable
{
public function isLoaded() : bool;
public function setLoaded(bool $loaded) : TransientLoadable;
public function setTransientLoadingFunction(\Closure $loadingFunction) :
TransientLoadable;
}
Implement the entity
class BlogPost implements TransientLoadable
{
...
}
Setup Loading function on postLoad Event
public function postLoad(BlogPost $post)
{
$func = function() use ($postHandler, $post)
{
//Since there may be another fields being loaded from the same API, catch them also since data is anyway in the same request
$postHandler->setAllDataFromAPI($post)
//Set the loading state to true to prevent calling the API again for the next property which may also be transient
$post->setLoaded(true);
}
$post->setTransientLoadingFunction($func)
}
Use the built-in lazy loading mechanism to get the property from the API only when it's needed
class BlogPost implements TransientLoadable
{
private function getStatus() : int
{
if (!$this->isLoaded) {
call_user_function($this->loadingFunction)
}
return $this->status;
}
private function getVisitorCount() : int
{
if (!$this->isLoaded) {
call_user_function($this->loadingFunction)
}
return $this->visitorCount;
}
}
So what's happening? Let's imagine we want to get the status and the visitor count, both are loaded via a single external API call.
If some api-dependent property of the entity is needed, all other properties gets loaded too (since we don't want to have for each property another call). This in ensured through the loaded function of the TransientLoadable interface. All data gets loaded by the setAllDataFromAPI function which is injected as a closure function.
I think that is not the cleanest solution. The loading stuf should be done by an extra layer on top of the entity class. Since sonata admin does not deal with such an layer, i think that this solution is cleaner than writing the loading mechanism directly to the entity class.
I am open to another suggestions or feedback
Thanks
After research and watch over the docs and other links, i understand perfectly why this can't be done the "old way", but i really need a workaround/solution for this:
I have a base class, it's not called directly on the request (not declared on route):
class AdminPanelController extends Controller
{
protected $_authUser;
public function __construct() {
$this->middleware(function ($request, $next) {
$this->_authUser = Auth::guard('admin')->user();
dd($this->_authUser); // ok
return $next($request);
});
}
...
protected function yoo() {
dd($this->_authUser); // auth user is null, but i need this here
}
}
I need for the authenticated user to be available on the yoo() method. The controller called directly:
Route::get('users', 'UsersController#hey');
UsersController:
class UsersController extends AdminPanelController
{
protected params;
public function __construct(Request $request) {
parent::__construct();
$this->params = $this->yoo(); // auth user is null, but i need this here
}
...
}
Note that if i call the method $this->yoo() on another place instead of the constructor the user would be available
NOTE: i also tried $request->user() (Authentication user provider [] is not defined.) but since i have a multi auth system i have to provide a guard and tried $request->guard('admin')->user(), with the result being Method guard does not exist.
I have 3 services which should override the default services only if the user has a specific role.
Or even better. Inject the current user/security in the new services.
The service then performs the check for the user role and calls the original service.
I tried to inject security.context into it. But then $security->getToken() returns null.
In the controllers it works fine. How can i get the current user in my service? This is what i want to do:
class AlwaysVisibleNavigationQueryBuilder extends NavigationQueryBuilder
{
public function __construct(\Sulu\Component\Content\Compat\StructureManagerInterface $structureManager, $languageNamespace, SecurityContext $security)
{
if (in_array('ROLE_SULU_ADMINISTRATOR', $security->getToken()->getRoles())) {
// Show unpublished content, too
$this->published = false;
}
parent::__construct($structureManager, $languageNamespace);
}
}
At the moment of creation of the service, the securityContext was not aware of the current user. The Security is filles when the application runs and not on dependency-resolution.
The following Code works.
class AlwaysVisibleNavigationQueryBuilder extends NavigationQueryBuilder
{
protected $security;
public function __construct(\Sulu\Component\Content\Compat\StructureManagerInterface $structureManager, $languageNamespace, SecurityContext $security)
{
$this->security = $security;
parent::__construct($structureManager, $languageNamespace);
}
public function build($webspaceKey, $locales)
{
$roles = $this->security->getToken()->getRoles();
if (in_array('ROLE_SULU_ADMINISTRATOR', $roles)) {
// Show unpublished content, too
$this->published = false;
}
return parent::build($webspaceKey, $locales);
}
}
Thanks to Matteo!
supposed having certain route string like "/path/index.html" protected by firewall, how to chek whether current user is able to access it?
Thanks in advance!
I am sorry, I should have been more explicit: I have an array of route names and I construct a menu. A lot of users with different roles can access a page with this menu. The purpose is to show only accessible liks in this menu for a particular user.
Something like:
'security_context'->'user'->isGranted('/path/index.html')
This answer is based on your comments:
You should get the roles needed to access that route.to that you need access to the security.access_map service which is private.so it has to be injected directly.e.g: you can create a path_roles service like such that you can get the roles for a certain path:
namespace Acme\FooBundle;
class PathRoles
{
protected $accessMap;
public function __construct($accessMap)
{
$this->accessMap = $accessMap;
}
public function getRoles($path)
{ //$path is the path you want to check access to
//build a request based on path to check access
$request = Symfony\Component\HttpFoundation\Request::create($path, 'GET');
list($roles, $channel) = $this->accessMap->getPatterns($request);//get access_control for this request
return $roles;
}
}
now declare it as a service:
services:
path_roles:
class: 'Acme\FooBundle\PathRoles'
arguments: ['#security.access_map']
now use that service in your controller to get the roles for the path and construct your menu based on those roles and isGranted.i.e:
//code from controller
public function showAction(){
//do stuff and get the link path for the menu,store it in $paths
$finalPaths=array();
foreach($paths as $path){
$roles = $this->get('path_roles')->getRoles($path);
foreach($roles as $role){
$role = $role->getRole();//not sure if this is needed
if($this->get('security.context')->isGranted($role)){
$finalPaths[] = $path;
break;
}
}
//now construct your menu based on $finalPaths
}
}
You could use security.access_control configuration option:
securty:
access_control:
- { path: "^/path/index.html$", roles: ROLE_SOME_ROLE}
Or simply check that manually from within your controller:
class SomeController extends Controller {
public function indexAction() {
if (!$this->get('security.context')->isGranted(...)) {
throw new AccessDeniedException(...);
}
...
}
}
I have a symfony2 web project consisting of ten pages which are rendered via 5 controllers. User can land a site through any of the pages above (via shared link for example). And I need to show a welcome popup (just div with position:absolute) to users who opens the page for the first time during the current session.
I've already placed my popup in the common twig template which is used by all the pages needed. Now I have to determine whether to show popup or not. I'm going to show popup based on boolean value from controller.
I have to work with session and cookies, but I have to do that on each page and I don't want to write the same code (check and set cookies, output a boolean to show popup in template) in every method of each controller. Is there a way to this according to DRY concepts?
You could make a wrapper class which handles checking, setting, and getting the current session values and make it a service.
<?php
namespace My\Bundle\My\Namespace;
use Symfony\Component\HttpFoundation\Session\Session;
class SessionManager /* or whatever you want to call it */
{
public function __construct(Session $session)
{
$this->session = $session;
}
public function getMyValue()
{
return $this->session->get('my.value.key',null);
}
public function setMyValue($value)
{
$this->session->set('my.value.key',$value);
return $this;
}
public function hasMyValue()
{
return $this->session->has('my.value.key');
}
}
And in your bundle services.yml
<service id="mytag.session_manager" class="My\Bundle\My\Namespace\SesionManager">
<argument type="service" id="session" />
</service>
And in your controllers
public function someAction()
{
$sessionManager = $this->get('mytag.session_manager');
if($sessionManager->hasMyValue())
{
// do something
}
}
Thanks to the Sgoettschkes answer here Where to place common business logic for all pages in symfony2
I tried this method
http://symfony.com/doc/current/book/templating.html#embedding-controllers
And it looks awesome:
My Popup is included in the main template like this
{{ render(controller('MalyutkaMainBundle:WelcomePopup:index')) }}
Than inside the controller I manipulate session vars
class WelcomePopupController extends Controller {
public function indexAction(Request $request) {
$session = $this->get('session');
$showWelcomePopup = 0;
if ($session->has("have_seen_welcome_popup_on")) {
// tbd compare to the date of publishing of the new popup screen
} else {
$showWelcomePopup = 1;
$session->set("have_seen_welcome_popup_on", new \DateTime());
}
$params = array (
'show_welcome_popup' => $showWelcomePopup
);
return $this->render('MalyutkaMainBundle:WelcomePopup:welcome_popup.html.twig', $params);
}
}
And nothing is to be added in other controllers - just what I wanted to do.
But it is impossible to change cookies that way, so I store my data in the session.