I am creating a SaaS with symfony2 providing private websites.
What I am trying to do is to let people access the website this way :
http://www.mydomain.com/w/{website_name}
Here is the routing configuration i am using :
websites:
resource: "#MyBundle/Resources/config/routing.yml"
prefix: /w/{website_name}
The problem is that when I try to access, for exemple, http://www.mydomain.com/w/chucknorris I am getting the error :
An exception has been thrown during the rendering of a template ("Some
mandatory parameters are missing ("website_name") to generate a URL
for route "websites_homepage".") in
"MyBundle:Publication:publicationsList.html.twig".
What I understood is that my route configuration is working well but when I am calling the router to generates url in the website it isn't aware of the "context" {website_name} url parameter.
One solution I've imagined is to find a way to automatically and seemlessly inject this parameter when it's set in the context.
Until now all I've been able to do is to create a service to get this parameter this way :
public function __construct(Registry $doctrine, ContainerInterface $container) {
$website_name = $container->get('request')->get("website_name");
if (!empty($website_name)) {
$repository = $doctrine->getManager()->getRepository('MyBundle:website');
$website = $repository->findOneByDomain($website_name);
if ($website) {
$this->website = $website;
} else {
throw new \Symfony\Component\HttpKernel\Exception\NotFoundHttpException();
}
} else {
$this->isPortal = true;
}
}
My question is : How do I inject that argument to all url generated to avoid getting the error of parameter missing and without having to specify it manualy everytime I call the router in controllers or twig ? (I guess it's something about request event but i have no clue on how to do it and especialy how to do it according to symfony2 good usages)
UPDATE
Here is the listener i created base on locallistener provided by symfony:
<?php
class WebsiteNameRouteEventListener implements EventSubscriberInterface {
private $router;
public function __construct(RequestContextAwareInterface $router = null) {
$this->router = $router;
}
public function onKernelResponse(FilterResponseEvent $event) {
$request = $event->getRequest();
$this->setWebsiteName($request);
}
public function onKernelRequest(GetResponseEvent $event) {
$request = $event->getRequest();
$this->setWebsiteName($request);
}
public static function getSubscribedEvents() {
return array(
// must be registered after the Router to have access to the _locale
KernelEvents::REQUEST => array(array('onKernelRequest', 16)),
KernelEvents::RESPONSE => 'onKernelResponse',
);
}
private function setWebsiteName(Request $request) {
if (null !== $this->router) {
echo "NEW CODE IN ACTION";die();
$this->router->getContext()->setParameter('website_name', $request->attributes->get("website_name"));
}
}
}
But i am still getting this error :
An exception has been thrown during the rendering of a template ("Some
mandatory parameters are missing ("website_name") to generate a URL
for route "homepage".") in
"MyBundle:Publication:publicationsList.html.twig". 500 Internal
Server Error - Twig_Error_Runtime 1 linked Exception:
MissingMandatoryParametersException »
Without my echo "...."; die() being executed so i guess twig is not firing the event i am listening on when it execute the path(routename) code.
Any idea ?
You could use the router context.
$this->router->getContext()->setParameter('website_name', $website_name);
See this file: https://github.com/symfony/symfony/blob/master/src/Symfony/Component/HttpKernel/EventListener/LocaleListener.php
Related
In a laravel php application I use the sentry to keep error info for example this controller:
class MuController
{
private function someMethodThatThrowsException()
{
throw new \Exception('Told ya');
}
public function foo()
{
try {
$this->someMethodThatThrowsException();
return new JsonResponse(204);
} catch(\Exception $e) {
app('sentry')->captureException($e);
return new JsonResponse(500);
}
}
}
I have setup my sentry as documentation says so:
use Sentry\Laravel\Integration;
....
public function register(): void
{
$this->reportable(function (Throwable $e) {
Integration::captureUnhandledException($e);
});
}
And I have exposed the sentry like this:
php artisan sentry:publish --dsn=___PUBLIC_DSN___
But sometimes I want some information from incomming http call to be hidden for security reasponse once reported to sentry. Is there a way to hide information from sentry regarding the http body?
I see that there's the functionality in https://docs.sentry.io/platforms/php/guides/laravel/configuration/filtering/ but Idk where this code should be places upon in my laravel project.
According to sentry's documentation you can set the following config at config/sentry.php:
return [
'dsn' => env('SENTRY_LARAVEL_DSN'),
// misc changed go here,
'before_send' => function (\Sentry\Event $event, ?\Sentry\EventHint $hint): ?\Sentry\Event {
$request = $event->getRequest();
// process request body here
$event->setRequest($request);
return $event;
}
];
For example you can remove any field in the body that contains password information:
return [
'dsn' => env('SENTRY_LARAVEL_DSN'),
// misc changed go here,
'before_send' => function (\Sentry\Event $event, ?\Sentry\EventHint $hint): ?\Sentry\Event {
$request = $event->getRequest();
// process request body here
foreach(['pass','password'] as $filtered){
if(isset($request['body'][$filtered])){
$request['body'][$filtered] = '[FILTERED]';
}
}
$event->setRequest($request);
return $event;
}
];
As you can see I use the $request['body'] and I check for any input, if input parameter matches then I replace the item with [FILTERED] therefore I avoid leaking sensitive info to 3rd party sentry.
Basically I have to write tests for many Laravel Controllers most of which are CRUD (read, store, update) and most of the logic is placed inside those(Inherited code, not mine).
What I need to do is automate the testing from a User's perspective. So I need to hit all the endpoints and test against a real database and check if everything turns out well.
I have almost no experience in testing, but from what I gather controllers should be tested with integration / acceptance tests. Now I did fine with testing Read methods by extending Laravel's TestCase, here is one example :
class SongsTest extends TestCase
{
public function testBasicIndex()
{
$arguments = [];
$response = $this->call('GET', '/songs', $arguments);
$this->assertResponseOk();
$this->seeJson();
}
/**
* #dataProvider providerSearchQuery
*/
public function testSearchIndex($query)
{
$arguments = ['srquery' => $query];
$response = $this->call('GET', '/songs', $arguments);
$this->assertResponseOk();
$this->seeJson();
}
public function providerSearchQuery()
{
return array(
array('a'),
array('as%+='),
array('test?Someqsdag8hj$%$')
);
}
public function testGetSongsById()
{
$id = 1;
$response = $this->call('GET', '/songs/' . $id);
$this->assertContains($response->getStatusCode(), [200, 404]);
$this->seeJson();
if($response->getStatusCode() == 404)
{
$content = json_decode($response->getContent());
$this->assertContains($content->message[0], ['Song not found', 'Item not active']);
}
}
}
These tests hit the endpoints and check if the response is 200 and the format is JSON and few other things. These work fine.
What I have problem with is :
Let's say for example we have a UserController, and a method that creates users. After that, said user should be used in TokensController to create a token that should be somehow remembered and used in future tests with token protected requests.
My question :
How do I automate : tests of UserController's store method by creating a real user in a testing database(without mocking), tests of TokensController's store method by using that user's email, testing other controllers with the created token and deleting that once the test is done, so it can be performed once again.
I just cannot conceptualize all that since I haven't really done much testing.
This is an example to use token and user's data for testing -
<?php
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;
class PostTest extends TestCase
{
use WithoutMiddleware;
public $token = "lrSFMf0DpqRAh4BXTXWHp5VgFTq4CqA68qY3jG2CqvcpNTT6m0y9Qs6OdpSn";
/*
A browser that receives a 302 response code HAS to redirect which means it will take the URL in the response and submit a new request. The result you see in your browser is the redirected page.
Unit testing does not redirect. Your unit test is only doing what you direct it to do. If your unit test should test for the redirect then you evaluate the response and the correct assertion is 302 and not 200.
*/
public function testExample()
{
$this->assertTrue(true);
}
public function testLogin()
{
$this->visit('/')
->type('abc#gmail.com', 'email')
->type('123456', 'password')
->press('Login') // type submit - value / button - lable
->seePageIs('/Wall'); // for redirect url
}
public function testFavourite()
{
$this->testLogin();
$request = [
'user_id' => '172',
'token' => $this->token,
'post_id' => '500'
];
$response = $this->call('POST', '/DoFavoriteDisc',$request);
$this->assertEquals(200, $response->getStatusCode());
}
}
Hope this will help you.
I'm turning to this forum because I can't find a valid solution to my problem.
I have taken over the management of a Symfony2 application which processes orders, invoices... inside a company and the problem is that there isn't archiving functions on it. So, the manager asked me to add archiving 'by year' functionalities to the application (simply display data depending on a chosen year).
So, I decided to prefix all application routes by /{year}/, parameter which will match the year the manager want to see and, as all the documents are dated, I just have to update Doctrine requests for picking those that match the chosen year. So far no problems.
routes.yml
mes_routes:
resource: "mes_routes.yml"
prefix: /{year}
defaults: {'year': %current_year%}
With this, I have created a Symfony Extension which fills the 'current_year' var by default in my route definition, with the actual year if no year is provided.
MyAppExtension.php
class MyAppExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.yml');
// Fill parameter with the current year
$container->setParameter('current_year', date("Y"));
}
}
Next, I have created a RouteListener that stores a previous route and its parameters inside a session var, when a user displays a new page (in order to display a same page but with a different year next)
LastRouteListener.php
class LastRouteListener
{
public function onKernelRequest(GetResponseEvent $event)
{
// Don't store subrequests
if ($event->getRequestType() !== HttpKernel::MASTER_REQUEST) {
return;
}
$request = $event->getRequest();
$session = $request->getSession();
$routeName = $request->get('_route');
$routeParams = $request->get('_route_params');
if ($routeName[0] == "_") {
return;
}
$routeData = ['name' => $routeName, 'params' => $routeParams];
// Don't store the same route twice
$thisRoute = $session->get('this_route', []);
if ($thisRoute == $routeData) {
return;
}
$session->set('last_route', $thisRoute);
$session->set('this_route', $routeData);
}
}
services.yml
myapp.last_route_event_listener:
class: MyApp\EventListener\LastRouteListener
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest, priority: 30 }
And finally, I have added a new controller which, via a dropdown menu in the application navbar, displays the current page the user is viewing, but with a different year
ArchiveController.php
class ArchiveController extends Controller
{
public function switchYearAction(Request $request, $year)
{
$session = $request->getSession();
$lastRoute = $session->get('last_route');
$route = $lastRoute["name"];
$routeParams = $lastRoute["params"];
if (array_key_exists("year", $routeParams)) {
$routeParams["year"] = $year;
$session->set("current_year", $year);
}
return $this->redirect($this->generateUrl($route, $routeParams));
}
}
Arrived here, everything work. If a user chose an other date, the application will display the same page but with the new date chosen.
However, and there is my problem, if, from a previous year, the user clicks on a link in the page, we come back to the actual year. Quite normal, because Twig paths in the application doesn't fill the 'year' routing parameter, and the router provide the current year by default.
So, my question is : How can I keep the chosen year in memory, and use it as a route parameter ?
First, I had thought about setting the local var 'current_year' when the application uses the switchYearAction(), but Symfony returns an error ('Frozen variable')
Next, I had thought about using a session var to store the chosen year, but I can't access to the session within my extension MyAppExtension.
There might be a third solution which consists in update all Twig paths and Controller redirect(), but it represents some much line to edit...
Or maybe with a routeEventListener... but I don't know how to proceed.
Thanks you in advance.
You can access the application session in Twig using {{ app.session }}. So something like this is possible:
{{ url('myapp.whatever', {year: app.session.get('current_year')}) }}
Since you have a bit of logic around your current year stuff (if it's not set in the session fallback to the current year, etc), a twig extension that provides a funtion to fetch the current year may be a better way to go. Quick, untested example:
<?php
use Symfony\Component\HttpFoundation\Session\SessionInterface;
class CurrentYearExtension extends \Twig_Extension
{
private $session;
private $defaultYear;
public function __construct(SessionInterface $session, $defaultYear)
{
$this->session = $session;
$this->defaultYear = $defaultYear;
}
public function getFunctions()
{
return [
new \Twig_SimpleFunction('current_year', [$this, 'currentYear']),
];
}
public function currentYear()
{
return $this->session->get('current_year') ?: $this->defaultYear;
}
}
Then add it to your container and tag it with the twig.extension tag.
<service id="myapp.currentyear_extension" class="CurrentYearExtension" public="false">
<argument type="service" id="session" />
<argument>%current_year%</argument>
<tag name="twig.extension" />
</service>
And use it in your route generation:
{{ url('myapp.whatever', {year: current_year()}) }}
If you need the current year other places than twig, then pull a re-usable object out of the twig extension and use that both with the extension and elsewhere.
Thanks to the answer given by #chrisguitarguy : https://stackoverflow.com/a/13495302/1031898 I found a way to resolve my problem.
In fact, I could use my routeListener to do the job.
I just needed to implement the Router Interface.
LastRouteListener.php (updated)
class LastRouteListener
{
private $router;
public function __construct(RouterInterface $router)
{
$this->router = $router;
}
public function onKernelRequest(GetResponseEvent $event)
{
// Don't store subrequests
if ($event->getRequestType() !== HttpKernel::MASTER_REQUEST) {
return;
}
$request = $event->getRequest();
$session = $request->getSession();
$context = $this->router->getContext();
// If current_year exists in session, replace route parameter with it
if ($session->has('current_year')) {
$context->setParameter('year', $session->get('current_year'));
}
// Else, we set the current year by default
else {
$context->setParameter('year', date('Y'));
}
$routeName = $request->get('_route');
$routeParams = $request->get('_route_params');
if ($routeName[0] == "_") {
return;
}
$routeData = ['name' => $routeName, 'params' => $routeParams];
// On ne sauvegarde pas la même route plusieurs fois
$thisRoute = $session->get('this_route', []);
if ($thisRoute == $routeData) {
return;
}
$session->set('last_route', $thisRoute);
$session->set('this_route', $routeData);
}
}
Don't forget to inject the #router argument in services.yml
myapp.last_route_event_listener:
class: MyApp\EventListener\LastRouteListener
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest, priority: 30 }
arguments: ['#router']
And then, no need to use 'current_year' default parameter in route config anymore.
routes.yml
mes_routes:
resource: "mes_routes.yml"
prefix: /{year}
MyAppExtension.php
class MyAppExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.yml');
}
}
I'm using the OAuth2 Server Bundle and I want to authenticate the user for all methods in my controller. It's very similar to the use case done in the documentation How to Setup before and after Filters, except that I want to return the response instead of throwing an exception.
onKernelController with FilterControllerEvent gives me access to the controller so I can access OAuth2 Server Bundle's response method. But I can't return any response in there. I can return a response in onKernelController using GetResponseEvent, but it gets called before onKernelController.
I looked into kernel.exception too but getResponse() returns different error messages so I rather not just throw an ambiguous exception.
What's the best practice for what I am trying to accomplish?
This is my code:
public function onKernelController(FilterControllerEvent $event)
{
$controller = $event->getController();
if ($controller[0] instanceof \Foo\Bundle\AuthBundle\Controller\TokenAuthenticatedController) {
$server = $controller[0]->get('oauth2.server');
$request = $controller[0]->get('oauth2.request');
$response = $controller[0]->get('oauth2.response');
if (!$server->verifyResourceRequest($request, $response)) {
return $server->getResponse();
}
}
}
public function onKernelRequest(GetResponseEvent $event)
{
$event->setResponse(new Response('Some response', 501));
}
In the EventListener Approach you can't return a response but you must set it on the event itself.
The problem is that the Event FilterControllerEvent can't manage a returned response, so a better approach is to manage(listen for) the GetResponseForControllerResultEvent where you can set a response and stop the propagation.
Hope this help.
I've followed the guide for implementing authentication/authorization and I can login.
I have one main difference though from what's in the guide. Instead of an isActive property I have a status table in my database.
I'm at a loss as to how I would deny/accept logins based on the values in the status table rather than the isActive property referenced in the guide.
I'm not sure what code to post because it works as it does in the guide and I'm pretty sure the Symfony security system handles all the authentication stuff where I can't see it.
Even if you just point me in the right direction I would be grateful.
Edit:
Using ChadSikorra's advice I came up with this code to implement the AdvancedUserInterface functions:
public function isAccountNonExpired()
{
$status = $this->getTblStatus()->getStatustext();
switch ($status){
case "expired":
return false;
default:
return true;
}
}
public function isAccountNonLocked()
{
$status = $this->getTblStatus()->getStatustext();
switch ($status){
case "locked":
return false;
case "suspended":
return false;
case "registered":
return false;
default:
return true;
}
}
public function isCredentialsNonExpired()
{
return $this->pwdexpired;
}
public function isEnabled()
{
$status = $this->getTblStatus()->getStatustext();
if($status != 'active')
return false
else
return true;
}
The next question I have then is how do I handle the exceptions that are thrown when a user has one of the statuses?
Based on what I have so far I think this is doable by catching the errors in the loginAction. What I don't know how to do is identify the errors, but I'll keep digging.
/**
* #Route("/Login", name="wx_exchange_login")
* #Template("WXExchangeBundle:User:login.html.twig")
* User login - Open to public
* Authenticates users to the system
*/
public function loginAction(Request $request)
{
$session = $request->getSession();
if ($this->get('security.context')->isGranted('IS_AUTHENTICATED_REMEMBERED'))
{
// redirect authenticated users to homepage
return $this->redirect($this->generateUrl('wx_exchange_default_index'));
}
// get the login error if there is one
if ($request->attributes->has(SecurityContext::AUTHENTICATION_ERROR)) {
$error = $request->attributes->get(
SecurityContext::AUTHENTICATION_ERROR
);
} else {
$error = $session->get(SecurityContext::AUTHENTICATION_ERROR);
$session->remove(SecurityContext::AUTHENTICATION_ERROR);
}
if($error instanceof LockedException)
{
}
return $this->render(
'WXExchangeBundle:User:login.html.twig',
array(
// last username entered by the user
'last_username' => $session->get(SecurityContext::LAST_USERNAME),
'error' => $error,
)
);
}
I am now able to check for the type of Exception, but I'm at a loss as to how to get the specific status so that I can redirect to the correct place. This is the last piece of the puzzle.
You could add mapping to your custom status table on the user entity, like so:
/**
* #ORM\OneToOne(targetEntity="AccountStatus")
* #ORM\JoinColumn(name="status_id", referencedColumnName="id", nullable=true)
*/
private $accountStatus;
This would also require creating an entity describing the custom status table. Then you could use this mapping in your user entity by implementing Symfony\Component\Security\Core\User\AdvancedUserInterface as referenced in the guide you linked. Then implement the isEnabled function something like this...
public function isEnabled()
{
return $this->getAccountStatus()->getIsActive(); /* Or whatever you named it */
}
EDIT:
Based on the API Doc for AdvancedUserInterface, if you want to do custom logic for handling the different statuses you'll need to register an exception listener...
If you need to perform custom logic for any of these situations, then
you will need to register an exception listener and watch for the
specific exception instances thrown in each case. All exceptions are a
subclass of AccountStatusException
There's a pretty good Cookbook article for creating something like this here. The basic process in this instance would be to create the class for the listener...
src/Acme/DemoBundle/EventListener/AcmeExceptionListener.php
namespace Acme\DemoBundle\EventListener;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Exception\DisabledException;
use Symfony\Component\Security\Core\Exception\LockedException;
class AcmeExceptionListener
{
public function onKernelException(GetResponseForExceptionEvent $event)
{
$exception = $event->getException();
if ($exception instanceof DisabledException) {
// Customize your response object to display the exception details
$response = new Response();
$response->setContent('<html><body><h1>Custom disabled page!</h1></body></html>');
// Send the modified response object to the event
$event->setResponse($response);
}
elseif ($exception instanceof LockedException) {
// Or render a custom template as a subrequest instead...
$kernel = $event->getKernel();
$response = $kernel->forward('AcmeDemoBundle:AccountStatus:locked', array(
'exception' => $exception,
));
$event->setResponse($response);
}
// ... and so on
}
}
The above are just basic examples, but it gives you the gist anyway. Technically I guess you could also make custom exceptions by extending AccountStatusException and then throw them in your logic for your AdvancedUserInterface implementation. Then you would know exactly which status you are catching. Anyway, then make sure to register the listener as a service.
app/config/config.yml
services:
kernel.listener.your_listener_name:
class: Acme\DemoBundle\EventListener\AcmeExceptionListener
tags:
- { name: kernel.event_listener, event: kernel.exception, method: onKernelException }
Another way to go about this would be to implement some sort of a custom User Checker. See this question: Symfony2 custom user checker based on accepted eula