Drupal 8 set cookie on every page load using event subscriber - php

I am trying to set a cookie on every page load. I did some research and found that event subscribers are the way to go.
I created a custom module with event. It starts the event on every page load but the problem is with the cookies.
The $event object has property called response but it is always null.
Therefore i cannot set any cookies.
class LanguageCookieSubscriber implements EventSubscriberInterface
{
protected $event;
protected $cookieValue;
public function init(GetResponseEvent $event) {
$this->event = $event;
$cookie = new Cookie("client_language_cookie", $this->cookieValue, 0, '/', NULL, FALSE);
$this->event->getResponse()->headers->setCookie($cookie);
}
}
I also tried to set the response object. Then I can set the cookie but the page will come blank.
$response = new Response();
$this->event->setResponse($response);
$cookie = new Cookie("client_language_cookie", $this->cookieValue, 0, '/', NULL, FALSE);
$this->event->getResponse()->headers->setCookie($cookie);
Any ideas how can i solve this? I need to display the page user has requested and only set cookie.
Thanks in advance.

Example:
public function onKernelResponse(ResponseEvent $event) {
$response = $event->getResponse();
$cookie = new Cookie('cookie name', 'my_cookie value', time() + 60 * 60 * 24 * 7);
$response->headers->setCookie($cookie);
$event->setResponse($response);
}

Related

how to assign a role programmatically to a symfony4 user

I am trying to create a route to programmatically assign a specific ROLE to current user. This is my attempt.
/**
* #Route("/role/assign/{role}", name="role_assignment")
*/
public function assign($role)
{
$session = $this->get('session');
$firewallContext = 'main';
$token = new UsernamePasswordToken(
'admin',
null,
$firewallContext,
array('ROLE_ADMIN')
);
$session->set('_security_'.$firewallContext, serialize($token));
$session->save();
$cookie = new Cookie($session->getName(), $session->getId());
$response = new JsonResponse([
'success' => 'true',
'user' => $this->getUser(),
]);
$response->headers->setCookie($cookie);
return $response;
}
User is always null, but I expected he become "admin" after page refresh.
I would strongly advice you against doing such things on production platforms. You will be better off properly configuring User Impersonation instead. It will save you the headache of having to manually do all of this.
If you really, really, want to go this way you could try the code below:
/**
* #Route("/role/assign/{username}/{role}", name="role_assignment")
*
* #param \Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface $tokenStorage
*
* #return JsonResponse
*/
public function assign($username, $role, TokenStorageInterface $tokenStorage)
{
// NOTES:
// 1. Make sure you are using the same User class as the one configured in `security.yml`
// 2. Keep in mind the $username MUST exist and MUST have the role you are setting,
// because the UserPasswordToken is reloaded from the session upon page refresh which triggers a check in the user provider and that will hit the database. In other words, if the user doesn't have `ROLE_ADMIN` you will most-likely get logged out or see "AccessDeniedException".
// For more information check \Symfony\Component\Security\Core\User\UserProviderInterface::refreshUser.
$user = new \Symfony\Component\Security\Core\User\User($username, null, array($role), true);
// Create token
$firewall = 'main'; // This MUST MATCH the name in your security.firewalls.->main<- or authentication WILL FAIL!
$usernamePasswordToken = new UsernamePasswordToken($user, null, $firewall, $user->getRoles());
// You don't need to save the token via $session->save().
// You can directly use $tokenStorage, which will do that for you.
$tokenStorage->setToken($usernamePasswordToken);
// Pass authentication to client.
return new JsonResponse(['success' => 'true', 'user' => $user]);
}
If you are trying to authenticate for test cases, you can have a look at my answer here which shows how you can configure a client which can authenticate as any user with any role you set (the user doesn't even have to exist in the db). This works fine for me on 3.4 so it should still work for 4.0.

Symfony cookie not sent (but in Response Header)

I can't understand why my cookie isn't sent.
I've check all the thread about it and it seems that I'm doing it right.
I can't see the cookies in the Symfony debug, but not on my browser.
public function indexAction(Request $request)
{
$response = new Response();
$userID = $request->cookies->get('myCookie123');
if (empty($userID)) {
$uniqueID = $this->generateIDUtil();
$response->headers->setCookie(new Cookie('myCookie123', $uniqueID, 2147483647));
$response->sendHeaders();
}else{
var_dump('USER ALREADY KNOW <br>' );
}
var_dump($response->headers->getCookies()); //GIVES CORRECT COOKIE OBJECT
var_dump('<br>');
var_dump($request->cookies->all()); //GIVES ONLY PHPSESSID, BUT NOT THE ONE CREATED
var_dump('<br>');
var_dump($request->cookies->get('myCookie123')); //GIVES NULL
return $response->setContent($this->render('MyBundle:Core:index.html.twig'));
}
So here a working piece of code for anyone that want to get a userId from a cookie. With all the use statement and the twig rendering template.
The problem with the solution above is that it will give you a blank page as you send the header without rendering the view.
As it's really badly documented, here is a working solution.
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class tempController extends Controller
{
public function indexAction(Request $request)
{
$user = $request->cookies->get('myCookie'); // $user is a string (the userID) or null if there is no cookie.
if (!empty($user)) {
//Fetch user data in the DB
}
// some code ...
$response = new Response();
if ($user instanceof User) {
// do some stuff like pre fill a form
}else{
// We don't know the user we send a cookie.
$cookie = new Cookie('myCookie', $this->generateIDUtil(), time() + (365 * 24 * 60 * 60)); // Expires 1 years
$response->headers->setCookie($cookie);
$response->sendHeaders();
}
//Render the view WITH the response (It's very important to put $response as 3rd parameter)
return $this->render('MyBundle:Core:index.html.twig', array(/* view param */), $response);
}
}
Here is how you properly create a cookie and send as a Response to a client:
$res = new Response();
$cookie = new Cookie(
'my_cookie',
$student->getStuId(),
time() + ( 2 * 365 * 24 * 60 * 60) // Expires 2 years.
);
$res->headers->setCookie( $cookie );
$res->send();
I use it all the time, so please let us know if you still have problems.

Slim 3 Framework + Cookies

I've been using Slim Framework 2 for a while but want to switch to the newest version 3. When reading the upgrade guide, I was a bit bummed about them simply stating that "cookies has been removed from the core" and referring to the FIG Cookies github repo that contains code snippets that simply don't work with Slim.
Could anyone share some working code snippets that set and get some dummy cookies using Slim 3? Thanks.
If you don't want to use the tested PSR-7 library FIG Cookies you can use this:
namespace Your\App;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
class Cookie
{
/**
* #param Response $response
* #param string $key
* #param string $value
* #return Response
*/
public function deleteCookie(Response $response, $key)
{
$cookie = urlencode($key).'='.
urlencode('deleted').'; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0; path=/; secure; httponly';
$response = $response->withAddedHeader('Set-Cookie', $cookie);
return $response;
}
/**
* #param Response $response
* #param string $cookieName
* #param string $cookieValue
* #return Response
*/
public function addCookie(Response $response, $cookieName, $cookieValue)
{
$expirationMinutes = 10;
$expiry = new \DateTimeImmutable('now + '.$expirationMinutes.'minutes');
$cookie = urlencode($cookieName).'='.
urlencode($cookieValue).'; expires='.$expiry->format(\DateTime::COOKIE).'; Max-Age=' .
$expirationMinutes * 60 . '; path=/; secure; httponly';
$response = $response->withAddedHeader('Set-Cookie', $cookie);
return $response;
}
/**
* #param Request $request
* #param string $cookieName
* #return string
*/
public function getCookieValue(Request $request, $cookieName)
{
$cookies = $request->getCookieParams();
return isset($cookies[$cookieName]) ? $cookies[$cookieName] : null;
}
}
Slim 3 has Cookies class. you are not forced to use external library for setting cookie:
$setcookies = new Slim\Http\Cookies();
$setcookies->set('auth',['value' => $jwt, 'expires' => time() + $expire, 'path' => '/','domain' => 'example.com','httponly' => true,'hostonly' => false,'secure' => true,'samesite' => 'lax']);
$setcookies->set('tracking', "$value");
$response = $response->withHeader('Set-Cookie', $setcookies->toHeaders());
And for getting cookie :
$jwt = $request->getCookieParam('auth');
I'was experiencing the same problem but, after a few tries, i figured out!
First you need to use :
$cookies = Dflydev\FigCookies\Cookies::fromRequest($request);
To get all the cookies sent by the client.
Or:
$cookie = FigRequestCookies::get($request, $cookiename);
To get a single cookie.
But the 'strange' part is how to set a cookie, so here's a little example:
function setCookie($response, $name, $value){
$response = FigResponseCookies::set($response, SetCookie::create($name)
->withValue($value)
->rememberForever()
);
return $response;
}
With :
$response = FigResponseCookies::set($response, SetCookie::create($name)
->withValue($value)
->rememberForever()
);
You'll add a new cookie to the request, this method returns a new request object with the new cookie in it.
So for all the other operations, you need to use the new request not the old one.
I hope this will help.
if you want post your code and we'll try to debug it.
I know I was asked to answer for cookies on slim 3, but since I could not find anywhere how to do this on slim 4, I was telling how to do it in case it could be useful to someone else.
If you are using Slim 4 with slim/psr7 you can set cookies from inside a middleware or a router, in this way:
To SET a cookie (put a cookie on a browser):
$cookies = new \Slim\Psr7\Cookies();
$cookies->setDefaults(['hostonly' => true, 'secure' => true, 'httponly' => true, 'samesite' => 'Lax']);
$cookies->set('MyCookieName', ['value' => '', 'samesite' => 'Strict']); // this way I can ovveride the samesite attribute for this cookie only
$response=$response->withHeader('Set-Cookie', $cookies->toHeaders());
unset($cookies);
To unset a cookie (delete a cookie from a browser)
$cookies = new \Slim\Psr7\Cookies();
$cookies->setDefaults(['hostonly' => true, 'secure' => true, 'httponly' => true, 'samesite' => 'Lax']);
$cookies->set('MyCookieName', ['value' => '', 'expires' => 1); // this way I can delete this cookie, setting it already expired the first sec in 1970
$response=$response->withHeader('Set-Cookie', $cookies->toHeaders());
unset($cookies);
To get the cookie (get the cookie sent from the browser)
$cookies = new \Slim\Psr7\Cookies($request->getCookieParams());
echo "cookie value is: " . $cookies->get('MyCookieName');
The easiest way I found to do this is as follows.
use Dflydev\FigCookies\SetCookie;
use Dflydev\FigCookies\FigResponseCookies;
$app->post('/login', function ($request, $response, $args) {
// Here you may check request data
$token = '123'; // Here you may use something like JWT::encode
return $this->response = FigResponseCookies::set($response, SetCookie::create('token')
->withValue($token)
->withDomain($_SERVER['HTTP_HOST'])
->withPath('/')
)->withJson([
'description' => 'Here goes your Json Body'
]);
});
Install vendor solution:
composer require sunrise/http-header-kit
Set the session cookie:
use Sunrise\Http\Header\HeaderSetCookie;
$header = new HeaderSetCookie('name', 'value', new \DateTime('+1 day'), [
'path' => '/',
'secure' => true,
'httponly' => true,
]);
$response = $header->addToMessage($response);
See the source code to learn more:
HTTP Header Kit for PHP 7.2+ based on PSR-7
I would recommend NOT using dflydev/dflydev-fig-cookies. It's broken and the documentation is either wrong or missing.
Based on LUXS's and Norman's answers, here's a working example with comments, for Slim 3:
/**
* Fig-cookies.
*
* Mind the required classes; mind the fact that (because PSR-7) this sets the
* cookie by modifying the response before returning it.
*
* Kudos to https://stackoverflow.com/a/55288070/1717535 and
* https://stackoverflow.com/a/42065030/1717535 for providing working examples.
*
* Memo about cookines in general:
* - if you don't set an expire date, it'll be a session cookie.
* - dot or no dot before the domain?
* - if no domain is specified, the cookie will be set without a dot.
* - if a domain is specified, it'll be the dotted version.
* - if both are set, seems the one set more recently will be used on read.
* - Seems the dotted/sub-domains version comes recommended.
* - But it forces to turn https://example.org into example.org, for all
* of local dev, ngrok, production, for no obvious benefit. So screw it.
* - Reconsider the day we'll use subdomains.
* - See https://stackoverflow.com/a/39708227/1717535
*
* Can use `->withDomain` to set the domain, without URL scheme and without
* trailing slash. e.g.: `->withDomain('clickandspeak.localhost')`
*/
use Dflydev\FigCookies\FigResponseCookies;
use Dflydev\FigCookies\SetCookie;
use Dflydev\FigCookies\FigRequestCookies;
$app->get('/tests/fig-cookies/set/', function ($request, $response, $args) {
$response = FigResponseCookies::set(
$response,
SetCookie::create('foo')
->withValue('bar')
->rememberForever()
->withPath('/')
);
// Do whatever else you need to do in your route…
// Then return the response.
return $response;
});
$app->get('/tests/fig-cookies/get/', function ($request, $response, $args) {
$cookie = FigRequestCookies::get($request, 'foo');
// but this is long and ugly and I would recommend Slim's (undocumented) built-in function instead:
// $request->getCookieParam('foo')
echo $cookie->getValue();
});
As for deleting/removing a cookie… Well:
$app->get('/tests/fig-cookies/delete/', function ($request, $response, $args) {
// $request = FigRequestCookies::remove($request, 'foo');
// $response = FigResponseCookies::expire($response, 'foo');
// Works. The cookie gets deleted.
// $response = FigResponseCookies::set(
// $response,
// SetCookie::create('foo')
// ->withExpires(new DateTime('-5 years'))
// ->withPath('/') // MUST use the same path as that used when creating the cookie.
// );
/**
* This is the example given in the README. But it does NOT work.
* It won't work because the path is missing; there's no option to set it!
* See https://github.com/dflydev/dflydev-fig-cookies/issues/23
*/
// $response = FigResponseCookies::expire($response, 'foo');
return $response;
});
I don't know if it will help anyone 4 years later but...
You can use the FIG Cookies library, turning it into a class with static functions. Like this:
namespace YourApp\Cookies;
use Dflydev\FigCookies\FigResponseCookies;
use Dflydev\FigCookies\SetCookie;
use Dflydev\FigCookies\FigRequestCookies;
use Dflydev\FigCookies\Modifier\SameSite;
use Carbon\Carbon; // I used Carbon as well
class Cookie
{
static function get(&$req, $key, $default = null)
{
$cookie = FigRequestCookies::get($req, $key, $default);
if (!$cookie->getValue()) {
return null;
}
return $cookie->getValue();
}
static function set(&$res, $key, $val, $expire_val, $expire_unit)
{
$now = Carbon::now();
$offset = Carbon::now()->add($expire_val, $expire_unit);
$res = FigResponseCookies::set($res, SetCookie::create($key)
->withValue($val)
->withExpires($offset->toCookieString())
->withMaxAge($offset->diffInSeconds($now))
->withPath('/')
->withDomain('appdomain.com')
->withSecure(false)
->withHttpOnly(true)
->withSameSite(SameSite::lax())
// You can learn more about the available options on the official GitHub page
);
return $res;
}
static function remove(&$res, $key)
{
$offset = Carbon::now()->sub(10, 'years');
$res = FigResponseCookies::set($res, SetCookie::create($key)
->withExpires($offset->toCookieString())
->withMaxAge(0)
->withPath('/')
->withDomain('appdomain.com')
);
return $res;
}
}
This way, it becomes really easy to deal with cookies.
You can use it like this:
use YourApp\Cookies\Cookie;
Cookie::set($response, 'name', 'value', 1, 'month') // To create a cookie
Cookie::get($request, 'name') // To get the value of a cookie
Cookie::remove($response, 'name') // To remove it
Or, if you prefer, like this:
\YourApp\Cookies\Cookie::set($response, 'name', 'value', 1, 'month')
\YourApp\Cookies\Cookie::get($request, 'name')
\YourApp\Cookies\Cookie::remove($response, 'name')
That's the way I use it.

"Cannot set session ID after the session has started." while testing form

I'm writing unit tests for my application. I wrote a function to login different user (to test user levels) and a function to generate valid or invalid form data (to test my form handling).
When the test submits a form, it throws an exception:
Uncaught PHP Exception LogicException: "Cannot set session ID after the session has started."
I'm using Symfony 2.6.4. I can't find any usefull information about this error message. The test worked perfectly a while ago.
class ControllerTest extends WebTestCase
{
public $client = null;
public $route = 'home/';
/**
* #var \Doctrine\ORM\EntityManager
*/
public $em;
public function setUp()
{
self::bootKernel();
$this->client = static::createClient();
$this->em = static::$kernel->getContainer()
->get('doctrine')
->getManager()
;
}
public function logIn($role = 'admin')
{
if ($role === 'admin') {
$userId = 20;
} elseif ($role === 'user') {
$userId = 29;
}
$user = $this->em->getRepository('Acme\DemoBundle\Entity\User')->find($userId);
$session = $this->client->getContainer()->get('session');
$firewall = 'main';
$token = new UsernamePasswordToken($user, $user->getPassword(), $firewall);
$session->set('_security_'.$firewall, serialize($token));
$session->save();
$cookie = new Cookie($session->getName(), $session->getId());
$this->client->getCookieJar()->set($cookie);
}
public function getFormData($valid = true)
{
//function to generate (in)valid formdata
}
public function getFormRequest($data, $url)
{
return $this->client->request(
'POST',
$url,
$data,
[],
[
'CONTENT_TYPE' => 'application/json',
'HTTP_X-Requested-With' => 'XMLHttpRequest',
]
);
}
//works OK
public function testNewScenario()
{
$url = $this->baseurl . 'new';
$this->logIn('admin');
$crawler = $this->client->request('GET', $url);
$this->assertEquals(200, $this->client->getResponse()->getStatusCode(), "Unexpected HTTP status code for GET " . $url);
}
public function testValidNewScenario()
{
$this->logIn('admin');
$validData = $this->getFormData(true);
//this function throws the exception
$this->getFormRequest($validData, $this->baseurl);
$this->assertEquals(200, $this->client->getResponse()->getStatusCode(), "Unexpected HTTP status code for POST " . $this->baseurl);
}
}
Here's the relevant part of my config_test.yml:
framework:
test: ~
session:
storage_id: session.storage.mock_file
profiler:
collect: false
What's going on?
I don't know if this is still a problem for the OP as this is an old post, but the same issue had me running around in circles for best part of 3 hours trying to find a way out of it. And seeing as there doesnt seem to be a solution anywhere at all. Heres a possible one.
The problem exists in tests which are trying to create a full login.
Current symfony docs state that its preferred to use basic_http authentication in your tests, but if, like me, you need to be testing access levels youll need to be following this method.
The problem seems to occur when we try to set the cookieJar up. This (for me) always threw an error.
Cannot set session ID after the session has started
the solution as it turns out is reasonably simple. Wrap the cookie set code in a condition that checks for a current session id.
if( !$this->session->getId() ) {
$this->cookie = new Cookie( $this->session->getName(), $this->session->getId() );
$this->client->getCookieJar()->set( $this->cookie ); // <--- this is the problem line
}
its also worth noting that calling $this->session->invalidate() does not solve the issue.
I hope this helps someone and saves them some time.
This effected me on Symfony2.1 (no chance of upgrading), but Ive seen mentions of 2.6 getting it when combined with FOSFacebookBundle (where I believe the issue was fixed).

Symfony2 Cookies not being set in controller

I can't seem to get cookies set from my controller.
code:
/**
* #Route("", name="wx_exchange_default_index")
* #Template("WXExchangeBundle:Default:index.html.twig")
* #Method({"GET"})
*/
public function indexAction(Request $request)
{
$returnArray['parents'] = self::getCategories();
$cookieLocationSession = $request->cookies->get('locationidsession', 0);
$cookieLocation = $request->cookies->get('locationid', 0);
$securityContext = $this->container->get('security.context');
$hashids = $this->get("wxexchange_hashids_service");
if ($securityContext->isGranted('IS_AUTHENTICATED_REMEMBERED'))
{
$user = $securityContext->getToken()->getUser();
$returnArray['user'] = $user;
$location = $user->getTblProfile()->getTblLocation();
if(!$cookieLocation || $cookieLocation != $hashids->encrypt($location->getId()))
{
$cookieLocation = $hashids->encrypt($location->getId());
//TODO: cookies are not being set figure out whys
$response = new Response();
$response->headers->setCookie(new Cookie("locationid", $cookieLocation, 365, '/', null, false, false));
$response->sendHeaders();
}
}
if (!$cookieLocation && !$cookieLocationSession)
{
return $returnArray;
}
if ($cookieLocationSession)
{
$cookieLocation = $cookieLocationSession;
}
if (!isset($location) || $cookieLocationSession)
{
$locationService = $this->get("wxexchange_location_service");
$locationId = $hashids->decrypt($cookieLocation);
if(count($locationId) >= 1)
$location = $locationService->getLocationById($locationId[0]);
}
if(isset($location))
return $this->redirect($this->generateUrl('wx_exchange_location', array('slug' => $location->getSlug(), 'locationid' => $cookieLocation)));
return $returnArray;
}
Do I have to return the response? If I do how do I keep processing (I have a redirect further down in my code)?
Edit: Interestingly enough if the same cookie is already set (via JQuery) running the code above deletes it, but it won't set it.
Edit: Action code posted.
The cookie object has httpOnly set to true by default, http://api.symfony.com/2.0/Symfony/Component/HttpFoundation/Cookie.html
This means that the browser should not make the cookie visible to client-side scripts. If you need to see the cookie in your scripts you can pass the 7th parameter as false when you create the cookie.
$response->headers->setCookie(new Cookie('foo', 'bar',time() + 60, '/', null, false, false));
If you just need to view the cookie for debugging purposes you can use Chrome Dev tools. They are available under the 'Resources' tab.
Edit : Try $response->sendHeaders();
Oops, Sorry everyone.
It's my error.
In my javascript I'm using a Jquery cookie plugin and when you set a new cookie you tell it the number of days before expiry:
$.cookie('locationid', value, { expires: 365, path: '/' });
Unfortunately I used a part of this syntax in my controller:
$cookie = new Cookie("locationid", $cookieLocation, 365, '/', null, false, false);
The problem is the third parameter is supposed to be a DateTime so while I thought I was telling it to expire in 365 days I probably created a cookie that expired almost instantly after creation.
Thanks for all the answers and time spent on your part, this is why I love SO.

Categories