Can we dispatch an event from Rabbitmq Consumer in Symfony 4.3? - symfony

I'm trying to dispatch a custom event from the Rabbitmq Consumer. This process used to work on Symfony 3.4, recently upgraded the project to Symfony Flex(4.3).
DatasetSubmissionConsumer.php
use OldSound\RabbitMqBundle\RabbitMq\ConsumerInterface;
/**
* A consumer of dataset submission messages.
*
* #see ConsumerInterface
*/
class DatasetSubmissionConsumer implements ConsumerInterface
{
/**
* The entity event dispatcher.
*
* #var EntityEventDispatcher
*/
protected $entityEventDispatcher;
/**
* Constructor.
*
* #param EntityEventDispatcher $entityEventDispatcher The entity event dispatcher.
*/
public function __construct(
EntityEventDispatcher $entityEventDispatcher,
) {
$this->entityEventDispatcher = $entityEventDispatcher;
}
/**
* Process a filer message.
*
* #param AMQPMessage $message A filer message.
*
* #return boolean True if success, false otherwise.
*/
public function execute(AMQPMessage $message)
{
$datasetSubmissionId = $message->body;
// Do Something //
$this->entityEventDispatcher->dispatch($datasetSubmission, 'dataset_processed');
return true;
}
}
DatasetSubmissionListener.php
/**
* Listener class for Dataset Submission-related events.
*/
class DatasetSubmissionListener
{
/**
* Method to send an email to DRPM on a dataset_processed event.
*
* #param EntityEvent $event Event being acted upon.
*
* #return void
*/
public function onDatasetProcessed(EntityEvent $event)
{
$datasetSubmission = $event->getEntity();
// Added if-statement so that emails are sent to data-managers only
// Do Something and send Email
}
}
EntityEventDispatcher.php
<?php
namespace App\Event;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* An entity event dispatcher.
*/
class EntityEventDispatcher
{
/**
* The event dispatcher to use in this entity event dispatcher.
*
* #var EventDispatcherInterface
*/
private $eventDispatcher;
/**
* Constructor.
*
* #param EventDispatcherInterface $eventDispatcher The event dispatcher to use.
*/
public function __construct(EventDispatcherInterface $eventDispatcher)
{
$this->eventDispatcher = $eventDispatcher;
}
/**
* Dispatch an Entity event.
*
* #param Entity $entity The Entity the event is for.
* #param string $entityEventName The name of the entity event.
*
* #return void
*/
public function dispatch(Entity $entity, string $entityEventName)
{
$this->eventDispatcher->dispatch(
'pelagos.entity.' . $entity->getUnderscoredName() . '.' . $entityEventName,
new EntityEvent($entity)
);
}
}
App\Event\DatasetSubmissionListener:
tags:
- { name: kernel.event_listener, event: pelagos.entity.dataset_submission.dataset_processed, method: onDatasetProcessed }
Also ran the bin/console debug:event-dispatcher and the event is callable
"pelagos.entity.dataset_submission.dataset_processed" event
-----------------------------------------------------------
------- ----------------------------------------------------------- ----------
Order Callable Priority
------- ----------------------------------------------------------- ----------
#1 App\Event\DatasetSubmissionListener::onDatasetProcessed() 0
------- ----------------------------------------------------------- ----------
Using Symfony 4.3, Rabbitmq 3.3.5
The Event Listener doesn't catch this dispatched event after the Rabbitmq Consumer exits with a success. Is there a way to debug or make this work?
Thanks.

I figured out that we can dispatch the event from Rabbitmq Consumer. The issue was with the SwiftMailer not sending emails from the dispatched event in the Event Listener.
DatasetSubmissionListener.php
/**
* Listener class for Dataset Submission-related events.
*/
class DatasetSubmissionListener
{
/**
* Method to send an email to DRPM on a dataset_processed event.
*
* #param EntityEvent $event Event being acted upon.
*
* #return void
*/
public function onDatasetProcessed(EntityEvent $event)
{
$datasetSubmission = $event->getEntity();
// Added if-statement so that emails are sent to data-managers only
// Do Something and send Email
sendEmailMessage($this->twig->load('Email/data-repository-managers.dataset-processed.email.twig'), $this->tokenStorage->getToken()->getUser());
}
/**
* Method to build and send an email.
*
* #param \Twig\TemplateWrapper $emailTwigTemplate A twig template.
* #param array $mailData Mail data array for email.
* #param array $toAddresses Recipient's email addresses.
* #param array $attachments An optional array of Swift_Message_Attachments to attach.
*
* #throws \InvalidArgumentException When any element of $attachments is not a Swift_Message_Attachment.
*
* #return void
*/
public function sendEmailMessage(
\Twig\TemplateWrapper $emailTwigTemplate,
array $mailData,
array $toAddresses = array(),
array $attachments = array()
) {
$message = new \Swift_Message();
$message
->setSubject($emailTwigTemplate->renderBlock('subject', $mailData))
->setFrom($this->from)
->setTo($toAddresses)
->setBody($emailTwigTemplate->renderBlock('body_html', $mailData), 'text/html')
->addPart($emailTwigTemplate->renderBlock('body_text', $mailData), 'text/plain');
foreach ($attachments as $attachment) {
if (!$attachment instanceof \Swift_Attachment) {
throw new \InvalidArgumentException('Attachment is not an instance of Swift_Attachment');
}
$message->attach($attachment);
}
$this->mailer->send($message);
}
}
The config which I had for swiftmailer previously:
swiftmailer:
url: '%env(MAILER_URL)%'
transport: sendmail
spool: { type: memory }
As I was having the spool feature enabled, the swiftmailer was waiting
for the kernel terminate event from the Rabbitmq Consumers, and the
consumers don't get terminated after acknowledging a message
To fix the issue, all I did was remove the spool feature.
swiftmailer:
url: '%env(MAILER_URL)%'
transport: sendmail

Related

Manually send password resetting email for user in FOSUserBundle

I have to create a simple user administration for a symfony 3 project.
One part of it is to start the password reset process for users.
(Yes, I know every user can trigger it himself but this is a request from our customer.)
Now I don't know how to start the process with a simple click in the admin interface for every user. Is there a method or a service in the UserBundle I can use?
There is no all in one method but this can be achieved by:
if (null === $user->getConfirmationToken()) {
$user->setConfirmationToken($this->tokenGenerator->generateToken());
}
// send email you requested
$this->mailer->sendResettingEmailMessage($user);
// this depends on requirements
$user->setPasswordRequestedAt(new \DateTime());
$this->userManager->updateUser($user);
with proper dependencies set.
Here's a service based solution written with Symfony 4.1 you can use without having to call in services form the container via get()
First you have to add an alias to services.yaml because the FOS mailer can't auto-wire:
FOS\UserBundle\Mailer\Mailer:
alias: fos_user.mailer.default
public: true
With that in place you can create the below class as service:
namespace App\Service; # change to your namespace
use FOS\UserBundle\Mailer\Mailer;
use FOS\UserBundle\Model\UserInterface;
use FOS\UserBundle\Model\UserManagerInterface;
use FOS\UserBundle\Util\TokenGeneratorInterface;
/**
* Class UserPasswordResetService
*/
class UserPasswordResetService
{
/**
* #var Mailer
*/
private $mailer;
/**
* #var UserManagerInterface
*/
private $userManager;
/**
* #var TokenGeneratorInterface
*/
private $tokenGenerator;
/**
* UserPasswordResetService constructor.
*
* #param Mailer $mailer
* #param UserManagerInterface $userManager
*/
public function __construct(
Mailer $mailer,
UserManagerInterface $userManager,
TokenGeneratorInterface $tokenGenerator
)
{
$this->mailer = $mailer;
$this->userManager = $userManager;
$this->tokenGenerator = $tokenGenerator;
}
/**
* #param UserInterface $user
*/
public function resetPassword(UserInterface $user)
{
if (null === $user->getConfirmationToken()) {
$user->setConfirmationToken($this->tokenGenerator->generateToken());
}
// send email you requested
$this->mailer->sendResettingEmailMessage($user);
// this depends on requirements
$user->setPasswordRequestedAt(new \DateTime());
$this->userManager->updateUser($user);
}
}
Assuming you then add that service to a class via DI you can use it like this within a given method:
$this->passwordResetService->resetPassword($user);
From the information Kamil provided, this would be a full working function
/**
* Sends the user a new password
*
* #Route("reset_password/{id}", name="user_reset_password")
* #Security("has_role('ROLE_ADMIN')")
*
* #param User $user
* #return \Symfony\Component\HttpFoundation\RedirectResponse
*/
public function resetPasswordAction(User $user)
{
if (null === $user->getConfirmationToken()) {
/** #var $tokenGenerator TokenGeneratorInterface */
$tokenGenerator = $this->get('fos_user.util.token_generator');
$user->setConfirmationToken($tokenGenerator->generateToken());
}
$this->get('fos_user.mailer')->sendResettingEmailMessage($user);
$user->setPasswordRequestedAt(new \DateTime());
$this->get('fos_user.user_manager')->updateUser($user);
$this->addFlash('notice', "User {$user->getFullName()} got an email for resetting his password!");
return $this->redirectToRoute('user_index');
}

Attach the user in session to the current EntityManager when writing functional tests

I'm writing a functional test for an Action entity having a relationship with the User entity:
<?php
namespace Acme\AppBundle\Entity;
/**
* Class Action
*
* #ORM\Table()
* #ORM\Entity(repositoryClass="Acme\AppBundle\Repository\ActionRepository")
*/
class Action
{
/**
* #var int
*
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var \Acme\AppBundle\Entity\User
*
* #ORM\ManyToOne(targetEntity="\Acme\AppBundle\Entity\User", inversedBy="actions")
* #ORM\JoinColumn(name="user_id", referencedColumnName="id")
*/
private $createdBy;
}
User:
namespace Acme\AppBundle\Entity;
/**
* #ORM\Entity
* #ORM\Table(name="`user`")
*/
class User extends BaseUser
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #var ArrayCollection
*
* #ORM\OneToMany(targetEntity="Action", mappedBy="createdBy")
*/
private $actions;
}
And the user is setted in the controller with the following snippet:
<?php
namespace Acme\ApiBundle\Controller;
/**
*
* #Route("/actions")
*/
class ActionController extends FOSRestController
{
public function postAction(Request $request)
{
$action = new Action();
$action->setCreatedBy($this->getUser());
return $this->processForm($action, $request->request->all(), Request::METHOD_POST);
}
}
When calling the action with a REST client for example, everything works fine, the relationship between Action and User is persisted correctly.
Now, when testing the action with a functional test, the relationship is not working because of the following error:
A new entity was found through the relationship 'Acme\AppBundle\Entity\Action#createdBy' that was not configured to cascade persist operations for entity: test. To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity or configure cascade persist this association in the mapping for example #ManyToOne(..,cascade={"persist"}).
For my functional test I need to inject a JWT and a session token because my routes are secured by a JWT and I need to have a user in session.
Here is how I inject that:
<?php
namespace Acme\ApiBundle\Tests;
class ApiWebTestCase extends WebTestCase
{
/**
* #var ReferenceRepository
*/
protected $fixturesRepo;
/**
* #var Client
*/
protected $authClient;
/**
* #var array
*/
private $fixtures = [];
protected function setUp()
{
$fixtures = array_merge([
'Acme\AppBundle\DataFixtures\ORM\LoadUserData'
], $this->fixtures);
$this->fixturesRepo = $this->loadFixtures($fixtures)->getReferenceRepository();
$this->authClient = $this->createAuthenticatedClient();
}
/**
* Create a client with a default Authorization header.
*
* #return \Symfony\Bundle\FrameworkBundle\Client
*/
protected function createAuthenticatedClient()
{
/** #var User $user */
$user = $this->fixturesRepo->getReference('user-1');
$jwtManager = $this->getContainer()->get('lexik_jwt_authentication.jwt_manager');
$token = $jwtManager->create($user);
$this->loginAs($user, 'api');
$client = static::makeClient([], [
'AUTHENTICATION' => 'Bearer ' . $token,
'CONTENT_TYPE' => 'application/json'
]);
$client->disableReboot();
return $client;
}
}
Now, the issue is that the injected UsernamePasswordToken contains a User instance which is detached from the current EntityManager, thus resulting in the Doctrine error above.
I could merge the $user object in the postAction method into the EntityManager but I don't want to do that because it means I modify my working code to make a test passes.
I've also tried directling merging the $user object in my test into the EntityManager like this:
$em = $client->getContainer()->get('doctrine')->getManager();
$em->merge($user);
But it's not working either.
So now, I'm stuck, I really don't know what to do except that I need to attach the user in session back to the current EntityManager.
The error message you are getting indicates that the EntityManager contained in the test client's container doesn't know about your User entity. This leads me to believe that the way you are retrieving the User in your createAuthenticatedClient method is using a different EntityManager.
I suggest you try to use the test kernel's EntityManager to retrieve the User entity instead. You can get it from the test client's container, for example.
Thanks to your tweet, I come to complete the given answer and (try to) propose a solution,
The problem is that your user is not managed by the EntityManager, and more simply, because it's not a real existing user that is registered in database.
To get around this problem, you need to have a real (managed) user that doctrine could use for the association that your action is trying to create.
So, you can either create this user at each execution of your functional test case (and delete it when finished), or create it only once when execute the test case for the first time on a new environment.
Something like this should do the trick:
/** #var EntityManager */
private $em;
/**
*/
public function setUp()
{
$client = static::createClient();
$this->em = $client->getKernel()
->getContainer()
->get('doctrine');
$this->authClient = $this->createAuthenticatedClient();
}
/**
*/
protected function createAuthenticatedClient()
{
/** #var User $user */
$user = $this->em
->getRepository('Acme\AppBundle\Entity\User')
->findOneBy([], ['id' => DESC]; // Fetch the last created
// ...
return $client;
}
That's a pity for your fixtures (that are so much sexier), but I don't see any way to attach your fixture as a real entry, as you can't interact more with the tested controller.
Another way would be to create a request to your login endpoint, but it would be even more ugly.

Disabled user by default on FOSUserbundle

I'm using FOSUserBundle and I want when each user registers to be disabled by default. The administrator will contact every user by phone and he will make user active if it's appropriate. I have read about Overriding Default FOSUserBundle Controllers but I can't figure out how to make it working. I have created RegistrationController.php in src/AppBundle/Controller/RegistrationController.php with this method inside:
<?php
/*
* This file is part of the FOSUserBundle package.
*
* (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\UserBundle\Controller;
use Symfony\Component\DependencyInjection\ContainerAware;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\Exception\AccountStatusException;
use FOS\UserBundle\Model\UserInterface;
/**
* Controller managing the registration
*
* #author Thibault Duplessis <thibault.duplessis#gmail.com>
* #author Christophe Coevoet <stof#notk.org>
*/
class RegistrationController extends ContainerAware
{
/**
* Receive the confirmation token from user email provider, login the user
*/
public function confirmAction($token)
{
$user = $this->container->get('fos_user.user_manager')->findUserByConfirmationToken($token);
if (null === $user) {
throw new NotFoundHttpException(sprintf('The user with confirmation token "%s" does not exist', $token));
}
$user->setConfirmationToken(null);
$user->setEnabled(false);
$user->setLastLogin(new \DateTime());
$this->container->get('fos_user.user_manager')->updateUser($user);
$response = new RedirectResponse($this->container->get('router')->generate('fos_user_registration_confirmed'));
$this->authenticateUser($user, $response);
return $response;
}
}
, but nothing works, maybe I need someone to show me the way to do it, nothing more.
For those still struggling with this question, set an observer listening the event: fos_user.registration.initialize like this (adapt you code path) :
app.listener.disable_registered_user:
class: AppBundle\Observer\DisableRegisteredUserListener
arguments:
- "#templating"
tags:
# split to multiple line for readability
# can be made into a single line like - { name: ..., event: ... , method: ... }
-
name: "kernel.event_listener"
event: "fos_user.registration.initialize"
method: "disableUser"
Then this is the content of your event listener class :
namespace AppBundle\Observer;
use FOS\UserBundle\Event\GetResponseUserEvent;
/**
* Class DisableRegisteredUserListener
* #package AppBundle\Observer
*/
class DisableRegisteredUserListener
{
/**
* #param \FOS\UserBundle\Event\GetResponseUserEvent $event
*/
public function disableUser(GetResponseUserEvent $event)
{
$user = $event->getUser();
/** #var \AppBundle\Entity\User $user */
$user->setEnabled(false);
}
}
You could just listen to the FOSUserEvents::REGISTRATION_CONFIRM and disable the registered user again before it gets persisted to the database.
As the FOSUserBundle automatically forwards the new user to the confirmedAction that requires a user to be logged in, you would need to provide your own response to override this.
Your listener...
class DisableRegisteredUserListener
{
/**
* #var EngineInterface
*/
private $templating;
/**
* #var EngineInterface $templating
*/
public function __construct(EngineInterface $templating)
{
$this->templating = $templating;
}
/**
* #var GetResponseUserEvent $event
* #return null
*/
public function disableUser(GetResponseUserEvent $event)
{
$user = $event->getUser();
$user->setEnabled(false);
$response = $this->templating->renderResponse(
'AppBundle:Registration:registration_complete.html.twig',
array(
'user' => $user,
)
);
}
}
Your services file (YAML)...
services:
app.listener.disable_registered_user:
class: AppBundle\EventListener\DisableRegisteredUserListener
arguments:
- "#templating"
tags:
# split to multiple line for readability
# can be made into a single line like - { name: ..., event: ... , method: ... }
-
name: "kernel.event_listener"
event: "fos_user.registration.confirm"
method: "disableUser"
Your AppBundle:Registration:registration_complete.html.twig could then be used to tell the new users that their account had been created but disabled and they would then be contacted by a member of your team to complete the process.

Paypal IPN not working, but posting data does

I am using the PayPal IPN simulator here: https://developer.paypal.com/webapps/developer/applications/ipn_simulator
to send information to an application built with symfony2 and payum bundle (older version of symfony and bundle).
It is definitely getting to the application at the notify URL (so not a firewall issue) because a record is stored in the database with the payment name and the date. However there are no 'details' stored.
However, if I use a Rest Client to POST to a URL with data, as suggested here: https://developer.paypal.com/docs/classic/ipn/integration-guide/IPNIntro/#id08CKFJ00JYK
Then the record is stored with payment name and date and details!!
Is this an issue with the IPN simulator? I'm really not sure what is going on here, maybe I could try and log the request object somehow?
#hsb1007 I think this was what I used finally. But I'm pretty sure there was some settings on the paypal side which was the main issue. I just remember doing lots and lots of testing and waiting
<?php
namespace LabIT\CMSBundle\EventListener;
use Buzz\Client\ClientInterface;
use Exception;
use LabIT\CMSBundle\Entity\Payments\DetailsInterface;
use LabIT\CMSBundle\Helper\ApiHelper;
use LabIT\CMSBundle\Helper\PaymentHelper;
use LabIT\CMSBundle\Helper\SubscriptionHelper;
use LabIT\CMSBundle\Helper\UserHelper;
use Payum\Core\Action\PaymentAwareAction;
use Payum\Core\Exception\RequestNotSupportedException;
use Payum\Core\Request\Notify;
use Payum\Paypal\Ipn\Api;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Log\LoggerInterface;
class PaymentListener extends PaymentAwareAction
{
/**
* #var UserHelper
*/
protected $userHelper;
/**
* #var PaymentHelper
*/
protected $paymentHelper;
/**
* #var ApiHelper
*/
protected $apiHelper;
/**
* #var SubscriptionHelper
*/
protected $subscriptionHelper;
/**
* #var LoggerInterface
*/
protected $logger;
/**
* #var
*/
protected $buzz;
/**
* #var
*/
protected $sandbox;
/**
* #var
*/
protected $paypalValidation;
/**
* #param UserHelper $userHelper
* #param PaymentHelper $paymentHelper
* #param ApiHelper $apiHelper
* #param SubscriptionHelper $subscriptionHelper
* #param LoggerInterface $logger
* #param ClientInterface $buzz
* #param $sandbox
* #param $paypalValidation
*/
public function __construct(
UserHelper $userHelper,
PaymentHelper $paymentHelper,
ApiHelper $apiHelper,
SubscriptionHelper $subscriptionHelper,
LoggerInterface $logger,
ClientInterface $buzz,
$sandbox,
$paypalValidation
) {
$this->userHelper = $userHelper;
$this->paymentHelper = $paymentHelper;
$this->apiHelper = $apiHelper;
$this->subscriptionHelper = $subscriptionHelper;
$this->logger = $logger;
$this->buzz = $buzz;
$this->sandbox = $sandbox;
$this->paypalValidation = $paypalValidation;
}
/**
* {#inheritDoc}
*
* This is where all the IPNs from paypal get processed, acts in some ways like a controller
*
* #param Notify $request
*/
public function execute($request)
{
$data = $_POST;
// would be better to get this dynamically. It is the payment name defined in config,
// but also forms part of the url set in the paypal notification backend
$paymentName = 'post_a_job_with_paypal'; // todo maybe get this from the url
$this->logger->notice('IPN received');
// validate with paypal so it stops notifying (do this first because needs to be done within 30 seconds)
if (true === $this->paypalValidation) {
$this->validateWithPaypal($this->getPaypalArray());
}
$notificationDetails = $this->paymentHelper->createPaymentNotification($paymentName, $data);
// todo other inspections of data? check email?
$user = $this->paymentHelper->getNotificationUser($notificationDetails, $data);
// these are only done for individual transactions //TODO STORE TRANSACTIONS IN TABLE?
if (isset($data['txn_id'])) {
$this->paymentHelper->getTransactionProcessed($data['txn_id']); // so don't process more than once //TODO ADD BACK IN AFTER ADDING TRANSACTION CLASS
$this->subscriptionHelper->determineUserMembership($user, $notificationDetails); // automatically demote if payment fails
$this->apiHelper->sendPaymentNotifications($user, $notificationDetails); // notify affiliates
$this->paymentHelper->setTransactionProcessed($data['txn_id']); //set transaction to processed //TODO ADD BACK IN AFTER ADDING TRANSACTION CLASS
}
// handle recurring payment data (recurring payment info, but not recurring payment transaction
if (isset($data['recurring_payment_id']) && !isset($data['txn_id'])) {
$this->paymentHelper->setRecurringPaymentStatus($data);
// cron job will determine user membership level because needs to check timestamp
}
}
/**
* {#inheritDoc}
*/
public function supports($request)
{
return $request instanceof Notify;
}
/**
* Send data back to paypal so paypal knows it was delivered successfully
*
* #param array $data
*
* #throws Exception
*/
protected function validateWithPaypal(array $data)
{
$this->logger->notice('I am here');
$options = array();
$options['sandbox'] = $this->sandbox;
$api = new Api($this->buzz, $options);
// Verify the IPN via PayPal
if (Api::NOTIFY_VERIFIED !== $api->notifyValidate($data)) {
$this->logger->notice('paypal validation UNSUCCESSFUL');
throw new Exception('Invalid IPN');
}
$this->logger->notice('paypal validation SUCCESSFUL');
return;
}
/**
* #return array
*/
protected function getPaypalArray()
{
// Read POST data
// reading posted data directly from $_POST causes serialization
// issues with array data in POST. Reading raw POST data from input stream instead.
$raw_post_data = file_get_contents('php://input');
$raw_post_array = explode('&', $raw_post_data);
$myPost = array();
foreach ($raw_post_array as $keyval) {
$keyval = explode ('=', $keyval);
if (count($keyval) == 2)
$myPost[$keyval[0]] = urldecode($keyval[1]);
}
return $myPost;
}

Symfony | FOSRestBundle with FOSFacebookBundle

I have a symfony app which serve a RESTful API(for mobile app) and have backend administration.
I can succesfuly login to the backend via facebook, but how should I allow loggin via the RESTful API?
Wooh.. after almost 12 hours(!) here is the solution for anyone who looking for too:
We need to create new custom firewall
This factory should connect to the FOSFacebook and validate the token
If it using our new firewall it should manually disable any session or cookie.
To use the firewall we need to send our token in every request
The code
First define our firewall listener
GoDisco/UserBundle/Security/Firewall/ApiFacebookListener.php
<?php
/**
* Authored by AlmogBaku
* almog.baku#gmail.com
* http://www.almogbaku.com/
*
* 9/6/13 2:17 PM
*/
namespace Godisco\UserBundle\Security\Firewall;
use FOS\FacebookBundle\Security\Authentication\Token\FacebookUserToken;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\SecurityContextInterface;
use Symfony\Component\Security\Http\Firewall\ListenerInterface;
use Symfony\Component\HttpFoundation\Session\Session;
/**
* API gateway through Facebook oAuth token: Firewall
*
* Class ApiFacebookListener
* #package Godisco\UserBundle\Security\Firewall
*/
class ApiFacebookListener implements ListenerInterface
{
/**
* #var \Symfony\Component\Security\Core\SecurityContextInterface
*/
protected $securityContext;
/**
* #var \Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface
*/
protected $authenticationManager;
/**
* #var Session
*/
protected $session;
/**
* #var string
*/
protected $providerKey;
public function __construct(SecurityContextInterface $securityContext, AuthenticationManagerInterface $authenticationManager, Session $session, $providerKey)
{
if (empty($providerKey)) {
throw new \InvalidArgumentException('$providerKey must not be empty.');
}
$this->securityContext = $securityContext;
$this->authenticationManager = $authenticationManager;
$this->session = $session;
$this->providerKey=$providerKey;
}
/**
* #param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event The event.
*/
public function handle(GetResponseEvent $event)
{
$accessToken = $event->getRequest()->get('access_token');
$token = new FacebookUserToken($this->providerKey, '', array(), $accessToken);
/**
* force always sending token
*/
$_COOKIE=array();
$this->session->clear();
try {
if($accessToken)
$returnValue = $this->authenticationManager->authenticate($token);
$this->securityContext->setToken($returnValue);
}
} catch(AuthenticationException $exception) {
if(!empty($accessToken))
$event->setResponse(new Response(array("error"=>$exception->getMessage()),401));
}
}
}
Than create a new security factory which calling our listener, and will connect the authentication to the FOSFacebookBundle.
GoDisco/UserBundle/DependencyInjection/Security/Factory/ApiFacebookFactory.php
<?php
/**
* Authored by AlmogBaku
* almog.baku#gmail.com
* http://www.almogbaku.com/
*
* 9/6/13 2:31 PM
*/
namespace GoDisco\UserBundle\DependencyInjection\Security\Factory;
use FOS\FacebookBundle\DependencyInjection\Security\Factory\FacebookFactory;
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\DefinitionDecorator;
/**
* API gateway through Facebook oAuth token: Factory
*
* Class ApiFacebookFactory
* #package GoDisco\UserBundle\DependencyInjection\Security\Factory
*/
class ApiFacebookFactory extends FacebookFactory
{
/**
* {#inheritdoc}
*/
public function getKey()
{
return 'api_facebook';
}
/**
* {#inheritdoc}
*/
public function addConfiguration(NodeDefinition $node)
{
$builder = $node->children();
$builder
->scalarNode('provider')->end()
->booleanNode('remember_me')->defaultFalse()->end()
;
foreach ($this->options as $name => $default) {
if (is_bool($default)) {
$builder->booleanNode($name)->defaultValue($default);
} else {
$builder->scalarNode($name)->defaultValue($default);
}
}
}
/**
* {#inheritdoc}
*/
protected function createEntryPoint($container, $id, $config, $defaultEntryPointId)
{
return null;
}
/**
* {#inheritdoc}
*/
protected function createListener($container, $id, $config, $userProvider)
{
$listenerId = "api_facebook.security.authentication.listener";
$listener = new DefinitionDecorator($listenerId);
$listener->replaceArgument(3, $id);
$listenerId .= '.'.$id;
$container->setDefinition($listenerId, $listener);
return $listenerId;
}
}
Defining the listener service, so we can inject the arguments
GoDisco/UserBundle/Resources/config/services.yml
services:
api_facebook.security.authentication.listener:
class: GoDisco\UserBundle\Security\Firewall\ApiFacebookListener
arguments: ['#security.context', '#security.authentication.manager', '#session', '']
Defining our new firewall!
app/config/security.yml
security:
api:
pattern: ^/api
api_facebook:
provider: godisco_facebook_provider
stateless: true
anonymous: true
main:
...
You need to implement oAuth authentication from your client app.
This was answered before:
How to restfully login, Symfony2 Security, FOSUserBundle, FOSRestBundle?

Resources