I followed this documentation for Edit In Place, and setup the Activator, and it works!
However, I will be using this on the production site and allowing access via a ROLE_TRANSLATOR Authorization. This is also working, but I don't want the web interface always "on"
How would I go about enabling it via some sort of link or toggle?
My thoughts, it would be simple to just add a URL parameter, like ?trans=yes and then in the activator;
return ($this->authorizationChecker->isGranted(['ROLE_TRANSLATOR']) && $_GET['trans'] == 'yes');
Obviously, $_GET would not work, I didn't even try.
How do I generate a link to simply reload THIS page with the extra URL parameter
How do I check for that parameter within the "Activator"
or, is there a better way?
The "proper" way to do this, as I have discovered more about "services" is to do the logic diectly in the RoleActivator.php file.
referencing documentation for How to Inject Variables into all Templates via Referencing Services I came up with the following solution;
src/Security/RoleActivator.php
<?php
namespace App\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException;
use Translation\Bundle\EditInPlace\ActivatorInterface;
use Symfony\Component\Translation\TranslatorInterface;
use Symfony\Component\HttpFoundation\RequestStack;
class RoleActivator implements ActivatorInterface
{
/**
* #var AuthorizationCheckerInterface
*/
private $authorizationChecker;
/**
* #var TranslatorInterface
*/
private $translate;
/**
* #var RequestStack
*/
private $request;
private $params;
private $path;
private $flag = null;
public function __construct(AuthorizationCheckerInterface $authorizationChecker, TranslatorInterface $translate, RequestStack $request)
{
$this->authorizationChecker = $authorizationChecker;
$this->translate = $translate;
$this->request = $request;
}
/**
* {#inheritdoc}
*/
public function checkRequest(Request $request = null)
{
if ($this->flag === null) { $this->setFlag($request); }
try {
return ($this->authorizationChecker->isGranted(['ROLE_TRANSLATOR']) && $this->flag);
} catch (AuthenticationCredentialsNotFoundException $e) {
return false;
}
}
public function getText()
{
if ($this->flag === null) { $this->setFlag(); }
return ($this->flag) ? 'linkText.translate.finished' : 'linkText.translate.start'; // Translation key's returned
}
public function getHref()
{
if ($this->flag === null) { $this->setFlag(); }
$params = $this->params;
if ($this->flag) {
unset($params['trans']);
} else {
$params['trans'] = 'do';
}
$queryString = '';
if (!empty($params)) {
$queryString = '?';
foreach ($params as $key => $value) {
$queryString.= $key.'='.$value.'&';
}
$queryString = rtrim($queryString, '&');
}
return $this->path.$queryString;
}
private function setFlag(Request $request = null)
{
if ($request === null) {
$request = $this->request->getCurrentRequest();
}
$this->flag = $request->query->has('trans');
$this->params = $request->query->all();
$this->path = $request->getPathInfo();
}
}
config\packages\twig.yaml
twig:
# ...
globals:
EditInPlace: '#EditInPlace_RoleActivator'
config\services.yaml
services:
# ...
EditInPlace_RoleActivator:
class: App\Security\RoleActivator
arguments: ["#security.authorization_checker"]
So What I added over and above the php-translation example is the getText and getHref methods and corresponding private variables being set in the checkRequest and read there after.
Now in my twig template (in the header) I just use
{% if is_granted('ROLE_TRANSLATOR') %}
{{ EditInPlace.Text }}
{% endif %}
Add the new keys to the translation files and your done. the trans=do query parameter is toggled on and off with each click of the link. You could even add toggling styles with a class name, just copy the getText method to something like getClass and return string a or b with the Ternary.
Related
I override (custom operation and service) the DELETE operation of my app to avoid deleting data from DB. What I do is I update a field value: isDeleted === true.
Here is my controller :
class ConferenceDeleteAction extends BaseAction
{
public function __invoke(EntityService $entityService, Conference $data)
{
$entityService->markAsDeleted($data, Conference::class);
}
...
My service :
class EntityService extends BaseService
{
public function markAsDeleted(ApiBaseEntity $data, string $className)
{
/**
* #var ApiBaseEntity $entity
*/
$entity = $this->em->getRepository($className)
->findOneBy(["id" => $data->getId()]);
if ($entity === null || $entity->getDeleted()) {
throw new NotFoundHttpException('Unable to find this resource.');
}
$entity->setDeleted(true);
if ($this->dataPersister->supports($entity)) {
$this->dataPersister->persist($entity);
} else {
throw new BadRequestHttpException('An error occurs. Please do try later.');
}
}
}
How can I hide the "deleted" items from collection on GET verb (filter them from the result so that they aren't visible) ?
Here is my operation for GET verb, I don't know how to handle this :
class ConferenceListAction extends BaseAction
{
public function __invoke(Request $request, $data)
{
return $data;
}
}
I did something; I'm not sure it's a best pratice.
Since when we do :
return $data;
in our controller, API Platform has already fetch data and fill $data with.
So I decided to add my logic before the return; like :
public function __invoke(Request $request, $data)
{
$cleanDatas = [];
/**
* #var Conference $conf
*/
foreach ($data as $conf) {
if (!$conf->getDeleted()) {
$cleanDatas[] = $conf;
}
}
return $cleanDatas;
}
So now I only have undeleted items. Feel free to let me know if there is something better.
Thanks.
Custom controllers are discouraged in the docs. You are using Doctrine ORM so you can use a Custom Doctrine ORM Extension:
// api/src/Doctrine/ConferenceCollectionExtension.php
namespace App\Doctrine;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use App\Entity\Conference;
use Doctrine\ORM\QueryBuilder;
final class CarCollectionExtension implements QueryCollectionExtensionInterface
{
public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null): void
{
if ($resourceClass != Conference::class) return;
$rootAlias = $queryBuilder->getRootAliases()[0];
$queryBuilder->andWhere("$rootAlias.isDeleted = false OR $rootAlias.isDeleted IS NULL);
}
}
This will automatically be combined with any filters, sorting and pagination of collection operations with method GET.
You can make this Extension specific to an operation by adding to the if statement something like:
|| $operationName == 'conference_list'
If you're not using the autoconfiguration, you have to register the custom extension:
# api/config/services.yaml
services:
# ...
'App\Doctrine\ConferenceCollectionExtension':
tags:
- { name: api_platform.doctrine.orm.query_extension.collection }
If you also want to add a criterium for item operations, see the docs on Extensions
I have a website made with Symfony 3.4 and within my actions I must check if the current user can edit the target product, something like this:
/**
* #Route("/products/{id}/edit")
*/
public function editAction(Request $request, Product $product)
{
// security
$user = $this->getUser();
if ($user != $product->getUser()) {
throw $this->createAccessDeniedException();
}
// ...
}
How can I avoid making the same check on every action (bonus points if using annotations and expressions)?
I am already using security.yml with access_control to deny access based on roles.
You can use Voters for this exact purpose. No magic involved. After creating and registering the Voter authentication will be done automatically in the security layer.
You just have to create the Voter class and then register it as a service. But if you're using the default services.yaml configuration, registering it as a service is done automatically for you!
Here is an example you can use. You may have to change a few items but this is basically it.
To read more visit: https://symfony.com/doc/current/security/voters.html
<?php
namespace AppBundle\Security;
use AppBundle\Entity\Product;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use AppBundle\Entity\User;
class ProductVoter extends Voter
{
const EDIT = 'EDIT_USER_PRODUCT';
protected function supports($attribute, $subject)
{
if($attribute !== self::EDIT) {
return false;
}
if(!$subject instanceof Product) {
return false;
}
return true;
}
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
/** #var Product $product */
$product= $subject;
$user = $token->getUser();
if (!$user instanceof User) {
// the user must be logged in; if not, deny access
return false;
}
return $this->belongsToUser($product, $user);
}
private function belongsToUser(Product $product, User $user)
{
return $user->getId() === $product->getUser()->getId();
}
}
You could try with a listener:
Check the action name,for example, if it is "edit_product", them continue.
Get the current logged User.
Get the user of the product entity.
Check if current user is different to Product user, if it is true, throw CreateAccessDeniedException.
services.yml
app.user.listener:
class: AppBundle\EventListener\ValidateUserListener
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
arguments: ["#service_container", "#doctrine.orm.entity_manager"]
Edit Action:
Added name "edit_product" to the action.
/**
*
* #Route("/products/{id}/edit",name="edit_product")
*/
public function editAction()
{
...
src\AppBundle\EventListener\ValidateUserListener.php
<?php
namespace AppBundle\EventListener;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
class ValidateUserListener
{
private $container;
private $entityManager;
public function __construct($container, $entityManager)
{
$this->container = $container;
$this->entityManager = $entityManager;
}
public function onKernelRequest(GetResponseEvent $event)
{
$currentRoute = $event->getRequest()->attributes->get('_route');
if($currentRoute=='edit_product' || $currentRoute=='edit_item' )
{
$array_user = $this->getCurrentUser();
if($array_user['is_auth'])
{
$current_user = $array_user['current_user'];
$product = $this->entityManager->getRepository('AppBundle:User')->findOneByUsername($current_user);
$product_user = $product->getUsername();
if ($current_user !==$product_user)
{
throw $this->createAccessDeniedException();
}
}
}
}
private function getCurrentUser()
{
//Get the current logged User
$user = $this->container->get('security.token_storage')->getToken()->getUser();
if(null!=$user)
{
//If user is authenticated
$isauth = $this->container->get('security.authorization_checker')->isGranted('IS_AUTHENTICATED_FULLY');
return array('is_auth'=>$isauth, 'current_user'=>$user);
}
return array('is_auth'=>false, 'current_user'=>$user);
}
}
Tested in Symfony 3.3
I need to migrate a really old version of Knp Menu to a newest one. The real problem is here
$collapse = new CollapseItem($group,$router->generate('seguridad_group_list'),array('class'=>'submenu'),'Primicia\SeguridadBundle\Menu\CollapseItem');
$collapse->setIcon('sp sp-ico-menu-grupo sp-icon-display');
$this->addChild($collapse);
How can I make it following the menu-as-service-way in the version 2 of KnpMenu?
The rest of code is this
The menu service implementation:
namespace Primicia\SeguridadBundle\Menu;
use Knp\Bundle\MenuBundle\Menu;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Router;
use Knp\Bundle\MenuBundle\Renderer\RendererInterface;
class SeguridadMenu extends Menu {
/**
* #param Request $request
* #param Router $router
* #param Container $container
*/
public function __construct(Request $request, Router $router, $container)
{
parent::__construct();
$this->setCurrentUri($request->getRequestUri());
$this->setAttribute('class','nav nav-list menu_lateral');
$translator = $container->get('translator');
if($container->get('security.context')->isGranted('ROLE_ADMIN'))
{
$user = $translator->trans('menu.user.titles',array(),'SeguridadBundle');
$signal = new CollapseItem($user,$router->generate('seguridad_user_list'),array('class'=>'submenu'),'Primicia\SeguridadBundle\Menu\CollapseItem');
$signal->setIcon('sp sp-ico-menu-usuario sp-icon-display');
$this->addChild($signal);
$group = $translator->trans('menu.group.titles',array(),'SeguridadBundle');
$collapse = new CollapseItem($group,$router->generate('seguridad_group_list'),array('class'=>'submenu'),'Primicia\SeguridadBundle\Menu\CollapseItem');
$collapse->setIcon('sp sp-ico-menu-grupo sp-icon-display');
$this->addChild($collapse);
}
}
/**
* Gets renderer which is used to render menu items.
*
* #return RendererInterface $renderer Renderer.
*/
public function getRenderer()
{
if(null === $this->renderer) {
if($this->isRoot()) {
$this->setRenderer(new ApcRenderer());
}
else {
return $this->getParent()->getRenderer();
}
}
return $this->renderer;
}
}
The CollapseItem class, which is used in the $router->generate
namespace Primicia\SeguridadBundle\Menu;
use Knp\Menu\MenuItem;
class CollapseItem extends MenuItem
{
protected $hasIcon;
public function renderLink()
{
$label = $this->renderLabel();
$uri = $this->getUri();
if (!$uri) {
die;
return sprintf('<a class="dropdown-toggle" href="#">%s</a>', $label);
}
return sprintf('<a class="dropdown-toggle" href="%s">%s</a>', $uri, $label);
}
public function setIcon($icon)
{
$this->hasIcon=$icon;
return $this;
}
public function getIcon()
{
return $this->hasIcon;
}
}
In Symfony2, the route parameters can be automatically map to the controller arguments, eg: http://a.com/test/foo will return "foo"
/**
* #Route("/test/{name}")
*/
public function action(Request $request, $name) {
return new Response(print_r($name, true));
}
see http://symfony.com/doc/current/book/routing.html#route-parameters-and-controller-arguments
But I want to use query string instead eg: http://a.com/test?name=foo
How to do that ?
For me there are only 3 solutions:
re-implement ControllerResolverInterface
use a custom ParamConverter
$name = $request->query->get('name');
Is there another solution ?
I provide you the code for those which want to use a converter :
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Sensio\Bundle\FrameworkExtraBundle\Request\ParamConverter\ParamConverterInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Put specific attribute parameter to query parameters
*/
class QueryStringConverter implements ParamConverterInterface{
public function supports(ParamConverter $configuration) {
return 'querystring' == $configuration->getConverter();
}
public function apply(Request $request, ParamConverter $configuration) {
$param = $configuration->getName();
if (!$request->query->has($param)) {
return false;
}
$value = $request->query->get($param);
$request->attributes->set($param, $value);
}
}
services.yml :
services:
querystring_paramconverter:
class: AppBundle\Extension\QueryStringConverter
tags:
- { name: request.param_converter, converter: querystring }
In your controller:
/**
* #Route("/test")
* #ParamConverter("name", converter="querystring")
*/
public function action(Request $request, $name) {
return new Response(print_r($name, true));
}
An improved solution based on Remy's answer which will map the parameter to an entity :
<?php
namespace AppBundle\Extension;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Component\HttpFoundation\Request;
use Sensio\Bundle\FrameworkExtraBundle\Request\ParamConverter\DoctrineParamConverter;
/**
* Put specific attribute parameter to query parameters
*/
class QueryStringConverter extends DoctrineParamConverter {
protected function getIdentifier(Request $request, $options, $name)
{
if ($request->query->has($name)) {
return $request->query->get($name);
}
return false;
}
}
services.yml:
services:
querystring_paramconverter:
class: MBS\AppBundle\Extension\QueryStringConverter
arguments: ['#doctrine']
tags:
- { name: request.param_converter, converter: querystring }
in your controller:
/**
* #Route("/test")
* #ParamConverter("myobject")
*/
public function action(Request $request, AnyEntity $myobject) {
return new Response(print_r($myobject->getName(), true));
}
like #2, To solve private method (getIdentifier) first set attributes and execute normally (parent::apply). Tested on Symfony 4.4
<?php
namespace App\FrameworkExtra\Converters;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Sensio\Bundle\FrameworkExtraBundle\Request\ParamConverter\DoctrineParamConverter;
use Symfony\Component\HttpFoundation\Request;
class QueryStringEntityConverter extends DoctrineParamConverter
{
public function supports(ParamConverter $configuration)
{
return 'querystringentity' == $configuration->getConverter();
}
public function apply(Request $request, ParamConverter $configuration)
{
$param = $configuration->getName();
if (!$request->query->has($param)) {
return false;
}
$value = $request->query->get($param);
$request->attributes->set($param, $value);
return parent::apply($request, $configuration);
}
}
I havn't checked, but it seems that the FOSRestBundle provides the #QueryParam annotation which does that :
http://symfony.com/doc/current/bundles/FOSRestBundle/param_fetcher_listener.html
I'm using the doctrine softdeleteable extension on a project and have my controller action set up as such.
/**
* #Route("address/{id}/")
* #Method("GET")
* #ParamConverter("address", class="MyBundle:Address")
* #Security("is_granted('view', address)")
*/
public function getAddressAction(Address $address)
{
This works great as it returns NotFound if the object is deleted, however I want to grant access to users with ROLE_ADMIN to be able to see soft deleted content.
Does there already exist a way to get the param converter to disable the filter or am I going to have to create my own custom param converter?
There are no existing ways to do it, but I've solved this problem by creating my own annotation, that disables softdeleteable filter before ParamConverter does its job.
AcmeBundle/Annotation/IgnoreSoftDelete.php:
namespace AcmeBundle\Annotation;
use Doctrine\Common\Annotations\Annotation;
/**
* #Annotation
* #Target({"CLASS", "METHOD"})
*/
class IgnoreSoftDelete extends Annotation { }
AcmeBundle/EventListener/AnnotationListener.php:
namespace AcmeBundle\EventListener;
use Doctrine\Common\Util\ClassUtils;
use Doctrine\Common\Annotations\Reader;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
class AnnotationListener {
protected $reader;
public function __construct(Reader $reader) {
$this->reader = $reader;
}
public function onKernelController(FilterControllerEvent $event) {
if (!is_array($controller = $event->getController())) {
return;
}
list($controller, $method, ) = $controller;
$this->ignoreSoftDeleteAnnotation($controller, $method);
}
private function readAnnotation($controller, $method, $annotation) {
$classReflection = new \ReflectionClass(ClassUtils::getClass($controller));
$classAnnotation = $this->reader->getClassAnnotation($classReflection, $annotation);
$objectReflection = new \ReflectionObject($controller);
$methodReflection = $objectReflection->getMethod($method);
$methodAnnotation = $this->reader->getMethodAnnotation($methodReflection, $annotation);
if (!$classAnnotation && !$methodAnnotation) {
return false;
}
return [$classAnnotation, $classReflection, $methodAnnotation, $methodReflection];
}
private function ignoreSoftDeleteAnnotation($controller, $method) {
static $class = 'AcmeBundle\Annotation\IgnoreSoftDelete';
if ($this->readAnnotation($controller, $method, $class)) {
$em = $controller->get('doctrine.orm.entity_manager');
$em->getFilters()->disable('softdeleteable');
}
}
}
AcmeBundle/Resources/config/services.yml:
services:
acme.annotation_listener:
class: AcmeBundle\EventListener\AnnotationListener
arguments: [#annotation_reader]
tags:
- { name: kernel.event_listener, event: kernel.controller }
AcmeBundle/Controller/DefaultController.php:
namespace AcmeBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use AcmeBundle\Annotation\IgnoreSoftDelete;
use AcmeBundle\Entity\User;
class DefaultController extends Controller {
/**
* #Route("/{id}")
* #IgnoreSoftDelete
* #Template
*/
public function indexAction(User $user) {
return ['user' => $user];
}
}
Annotation can be applied to individual action methods and to entire controller classes.
You can use #Entity for this, customizing a repository method like this:
use Symfony\Component\Routing\Annotation\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Entity;
/**
* #Route("/{id}")
* #Entity("post", expr="repository.findDisableFilter(id)")
*/
public function disable(Post $post): JsonResponse
{
...
}
and then in your repository class:
public function findDisableFilter(mixed $id): mixed
{
$filterName = 'your-filter-name';
$filters = $this->getEntityManager()->getFilters();
if ($filters->has($filterName) && $filters->isEnabled($filterName)) {
$filters->disable($filterName);
}
return $this->find($id);
}