Setting global session var in Slim4 and Twig - php

I'm been having issues with this for weeks on numerous plugins/extensions, so I am afraid I'm stuck.
It would be very helpful if I can use session data in all Twig templates, not just the ones parsed by Controllers via the Render method, like this which is working fine:
$current_session = $this->container->get('session');
return $this->container->get('twig')->render(
$response,
'/user/login.html.twig',
[
'title' => "Please login",
'session_user' => $current_session->user,
"session_id" => $current_session::id()
]
);
So here's what I have been trying for|with Slim/Session (by bryanjhv/slim-session) to get data straight into TWIG views. I've had similar issues with Slim/CSRF so I quit trying to add that due to mental sanity issues for trying too long ;-)
So, here's what I have in my Container file:
...
'twig' => function (Container $container) {
$twig = Twig::create(VIEW_PATH, [
'cache' => false,
'auto_reload' => true
]);
$twig->addExtension(new IntlExtension());
$environment = $twig->getEnvironment();
$environment->addGlobal('user', (object)['name' => 'xXx']);
return $twig;
},
// 'sessions'
'session' => function () {
return new Helper();
},
...
Session is started in middleware.php which is included via index.php
...
// Session
$app->add(new Session([
'autorefresh' => true,
// 'domain' => 'localhost',
// 'handler' => null,
'httponly' => false,
'lifetime' => '1 hour',
'name' => 'f_session',
'path' => '/',
'samesite' => 'Lax',
'secure' => false,
]));
...
Index.php
...
$app->add(new TwigSessionVarsMiddleware($container));
...
My entire TwigSessionVarsMiddleware.php
declare(strict_types=1);
namespace App\Middlewares;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\Psr7\Response;
use DI\Container;
class TwigSessionVarsMiddleware
{
protected $container;
public function __construct(Container $container)
{
$this->container = $container;
}
/**
* Example middleware invokable class
*
* #param ServerRequest $request PSR-7 request
* #param RequestHandler $handler PSR-15 request handler
*
* #return Response
*/
public function __invoke(Request $request, RequestHandler $handler): Response
{
$response = $handler->handle($request);
$existingContent = (string) $response->getBody();
$response = new Response();
// Get and Set Twig Globals
$this->container->get('twig')->getEnvironment()->getGlobals()['user'];
$current_user = $this->container->get('session');
$this->container->get('twig')->getEnvironment()->addGlobal("user", [
'id' => $current_user::id(),
'name' => $current_user->user,
]);
$userdata = $this->container->get('twig')->getEnvironment()->getGlobals()['user'];
$response->getBody()->write($userdata['id'] . $existingContent);
return $response;
}
}
The last 2 lines in the middleware are there for testing purposes, it will display the correct values on top of the template file as expected.
Also, the following is echo'ed just fine as well in the templates, however this is only the initial value set in the container, never the updated value in the middleware.
{{ user.name }}
I can not get any session data in my main index.php either, I've been struggling with the CSRF extension too and I'm afraid I am missing some key points. I must have been close at least a couple times after a gazillion tries.
Any input from anybody else? My Google search returned 8 (!) results that did not help.
*** UPDATE ***
Appently I'm really bad in figuring out how middleware works correctly. I moved the session part in middleware.php to the index and now sessions are available in Twig by default. Next I'll see if I can actually use CSRF as well.
...
AppFactory::setContainer($container);
/******************************************************
* Create App
*****************************************************/
$app = AppFactory::create();
/******************************************************
* Load Sessions
*****************************************************/
$app->add(
new Session([
'name' => 'f_session',
'autorefresh' => true,
'lifetime' => '1 hour',
])
);
...

Okay, turned out I only needed to change Slim\Psr7\Response; to Psr\Http\Message\ResponseInterface as Response;
CSRF integration still doesn't work though, but that's another challenge.

Related

How to inject customized info to Google Stackdriver in Laravel log

Hi I'm new to Laravel framework and I'm trying to inject process ID into every log passed to Google Logging service.
I've been able to see the log passed into Google Logging service now, but I've no idea how I could inject more info(Process ID in this case) into my log message.
So far I've tried "tap" method and I can see addition info injected into my log while reading laravel.log file, but same method doesn't seems to work while using Google Cloud Logging plugin.
Below is my script for Google Logging service.
Inside config/logging.php
'channels' => [
'stack' => [
'driver' => 'stack',
'channels' => ['stackdriver'],
],
'stackdriver' => [
'driver' => 'custom',
'via' => App\Logging\CreateStackdriverLogger::class,
'level' => 'debug',
],
'single' => [
'driver' => 'single',
'path' => storage_path('logs/laravel.log'),
'level' => 'debug',
'tap' => [App\Logging\ProcessorTap::class],
],
];
CreateStackdriverLogger.php
use Google\Cloud\Logging\LoggingClient;
use Monolog\Handler\PsrHandler;
use Monolog\Logger;
class CreateStackdriverLogger
{
/**
* Create a custom Monolog instance.
*
* #param array $config
* #return \Monolog\Logger
*/
public function __invoke(array $config)
{
$logName = isset($config['logName']) ? $config['logName'] : 'app';
$psrLogger = LoggingClient::psrBatchLogger($logName);
$handler = new PsrHandler($psrLogger);
$logger = new Logger($logName, [$handler]);
return $logger;
}
}
Code for 'tap' method, I'm able to see 'pid' inside 'extra', but same method don't work with 'stackdriver'.
ProcessorTab.php
namespace App\Logging;
use Illuminate\Log\Logger;
use Illuminate\Support\Arr;
use Monolog\Formatter\LineFormatter;
class ProcessorTap
{
public function __invoke($logger)
{
foreach ($logger->getHandlers() as $handler) {
$handler->pushProcessor(function ($record) {
return Arr::add($record, 'prefix', getmypid());
});
$handler->setFormatter($this->getLogFormatter());
$handler->pushProcessor([$this, 'processLogRecord']);
}
}
public function processLogRecord(array $record): array
{
$record['extra'] += [
'pid' => getmypid(),
];
return $record;
}
protected function getLogFormatter()
{
$format = "[%datetime%] %channel%.%level_name%: %prefix%.%message% %context% %extra%\n";
return new LineFormatter($format, null, true, true);
}
}

Laravel Tap Formatter: where do I actually format the message?

In my laravel application there's need to format a message I need to send into slack. Hence I set a slack log channel into config/logging.php:
'slack' => [
'driver' => 'slack',
'url' => /*Censored Hook URL*/,
'username' => 'MyApp',
'emoji' => ':gear:',
'level' => 'debug',
],
Also as seen on documentation I can do a monolog formater, hence I did the following:
namespace App\Logging;
class SlackLogFormatter
{
/**
* Customize the given logger instance.
*
* #param \Illuminate\Log\Logger $logger
* #return void
*/
public function __invoke($logger)
{
foreach ($logger->getHandlers() as $handler) {
$handler->setFormatter(...);
}
}
}
And specified it as tap into my log:
'slack' => [
'driver' => 'slack',
'tap' => [App\Logging\SlackLogFormatter::class]
'url' => /*Censored Hook URL*/,
'username' => 'MyApp',
'emoji' => ':gear:',
'level' => 'debug',
],
But in my formater where do I process the log entry itself? I mean:
The $handler->setFormatter does not seem to be a method of \Illuminate\Log\Logger class.
I cannot find out what method I need to override when I need to provide a custom format. I mean I have the invoke method then afterwards what?
Dimitrios is almost right (or perhaps this worked on older versions) but as Xavier said, if you copy that code exactly you will get the error
Return value of Monolog\Handler\AbstractProcessingHandler::processRecord() must be of the type array, null returned
You are going to want to do something like this instead :
<?php
namespace App\Logging;
use Monolog\Formatter\LineFormatter;
use Monolog\Handler\SlackWebhookHandler;
use Request;
class CustomiseFormatter
{
protected $request;
public function __construct(Request $request = null)
{
$this->request = $request;
}
public function __invoke($logger)
{
foreach ($logger->getHandlers() as $handler) {
if ($handler instanceof SlackWebhookHandler) {
$handler->setFormatter(new LineFormatter(
'[%datetime%] %channel%.%level_name%: %message% %context% %extra%'
));
$handler->pushProcessor([$this, 'processLogRecord']);
}
}
}
public function processLogRecord(array $record): array
{
$record['extra'] += [
'url' => env("APP_URL"),
];
return $record;
}
}
Equally, as with how extra has be modified, you can change any other value.
The formatter for slack should be the following:
namespace App\Logging;
use Monolog\Formatter\LineFormatter;
class SlackLogFormatter
{
/**
* Customize the given logger instance.
*
* #param \Illuminate\Log\Logger $logger
* #return void
*/
public function __invoke($logger)
{
foreach ($logger->getHandlers() as $handler) {
if ($handler instanceof SlackWebhookHandler) {
$format=""; // Look on the Monolog's Line formatter documentation
$formatter= new LineFormatter($format,"Y-m-d H:i:s");
$handler->pushProcessor(function ($record) {
//Append extra info of formatting here
});
$handler->setFormatter($formatter);
}
}
}
}
And config the slack not to send attachment nessesary for the formatter to work:
'slack' => [
'driver' => 'slack',
'tap' => [App\Logging\SlackLogFormatter::class]
'url' => /*Censored Hook URL*/,
'username' => 'MyApp',
'emoji' => ':gear:',
'level' => 'debug',
'attachment' => FALSE,
],
The setFormatter method takes a new Monolog Formatter as seen in: https://github.com/Seldaek/monolog/blob/master/doc/02-handlers-formatters-processors.md#formatters
Also the pushProcessor allows you to populate extra fields on your message eg. Showing an emoji on your log message:
public function __invoke($logger)
{
foreach ($logger->getHandlers() as $handler) {
if ($handler instanceof SlackWebhookHandler) {
$format="%emoji% %message%";
$formatter= new LineFormatter($format,"Y-m-d H:i:s");
$handler->pushProcessor(function ($record) {
$record['emoji']=":poop:";
});
$handler->setFormatter($formatter);
}
}
}

Setting Routes for RestApi in Yii2

I am trying to make rest api with my methods.
'rules' => [
[
'class' => 'yii\rest\UrlRule',
'controller' => ['ApiController'],
'patterns' => [
'PUT,PATCH api/{id}/update' => 'update',
'DELETE api/{id}/delete' => 'delete',
'GET,HEAD api/{id}' => 'get',
'POST api/{id}/create' => 'create',
'GET,HEAD' => 'api/index',
'{id}' => 'options',
'' => 'options',
]
],
Api controller:
/**
* Displays homepage.
*
* #return string
*/
public function actionIndex()
{
// $id = Yii::$app->request->getQueryParam("id"); //
Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
return "ok";
}
/**
* Displays homepage.
*
* #return string
*/
public function actionGet($id)
{
// $id = Yii::$app->request->getQueryParam("id"); //
Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
return "get";
}
Url api returns index action, but url api/1 doesn't return get action.
How to configure routing?
If you are ok with the default actions provided by yii you can simplify your code quite a bit to make it work.
Configure the response type on the application configuration, then you won't need to do it in each method.
Remove the 'patterns' element from your rules, yii automatically matches the patterns that you are trying to use.
Decide if you want to pluralize your rules or not, if you don't want to pluralize them you need to add 'pluralize' => false to your configuration rules.
web.config
// configure json response globally
'response' => [
'format' => Response::FORMAT_JSON,
'formatters' => [
Response::FORMAT_JSON => [
'class' => '\yii\web\JsonResponseFormatter',
'prettyPrint' => YII_DEBUG,
'encodeOptions' => JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE,
]
],
],
// Add ApiController rules
'rules' => [
[
'class' => 'yii\rest\UrlRule',
'controller' => 'api',
// uncoment the next line if you want to request '/api/2' instead of '/apis/2'
// 'pluralize' => false
],
// ... more rules here
]
ApiController
<?php
namespace app\controllers;
use yii\rest\Controller;
class ApiController extends Controller
{
public function actionIndex()
{
return 'Api index action';
}
public function actionView($id)
{
return "Get item $id";
}
}
Using the configuration provided you can request the index route sending a GET request to the /apis endpoint, to control the result customize actionIndex, you can provide a dataProvider as the response and the formatter element will deal with it correctly.
To request one element of the collection, send a GET request to the /apis/5 endpoint, where 5 is just an example $id, if you return a model, the formatter will deal with it using the fields attribute of the model.
If you want to use endpoints like in your question, i.e. without the plural form, uncomment the pluralize line on the example, the endpoints will be /api and /api/5.
There are multiple examples of this on the official documentation, the quick start and building a REST API pages make for some good reading and are packed with examples.
Personally I would recommend not naming a controller ApiController, it seems confusing, your API probably has api already on the url so you will end up with urls like https://api.mydomain.com/api/5

Bypass digest authentication middleware (zend expressive)?

So I'm working on an api that uses digest authentication middleware.
If a particular parameter is present in the request I want to be able to completely bypass authentication.
public function process(ServerRequestInterface $request, DelegateInterface $delegate)
{
/* TODO:: Figure out how to bypass the digest auth below */
/* Have tried: (if detect particular parameter) */
// return new Response\HtmlResponse(true);
// return new Response();
/* Begin digest authentication */
$authentication = new DigestAuthentication($this->credentials);
$authentication->realm($this->realm);
$authentication->nonce(uniqid());
return $authentication(
$request,
new Response(),
function ($request) use ($delegate) {
return $delegate->process($request);
}
);
}
Do I have the right idea here lads? Any help or suggestions welcome!
You have several options:
If the Api only has a few routes that need authentication you can manually add the middleware only for these routes, so the rest of them will not require authentication. E.g.:
'home' => [
'path' => '/',
'middleware' => [YourAuthenthicationMiddleware::class, HomePageHandler::class],
'allowed_methods' => ['GET'],
'name' => 'home',
],
If there are a few routes that don't need authentication you can put them in a path that is different than the one from the Apis and add this pipeline:
$app->pipe('/api', YourAuthenthicationMiddleware::class);
No auth path: /myApp/any/path
Auth path: /api/any/path
Set a key for each route and check it in the authentication middleware
Route:
'login' => [
'path' => '/login[/]',
'middleware' => LoginHandler::class,
'allowed_methods' => ['GET', 'POST'],
'name' => 'login',
'authentication' => [
'bypass' => true,
],
],
AuthenticationMiddleware:
$this->routeConfiguration = $config['routes'];
$routeResult = $request->getAttribute(RouteResult::class);
...
if (empty($this->routeConfiguration[$routeResult->getMatchedRouteName()]['authentication']['bypass'])) {
//try to authenticate
}
For the last option make sure that this pipe is injected:
$app->pipe(RouteMiddleware::class);

How do I change security redirect behavior in Silex?

If I make an AJAX request to a secured area, it sends a 302 header and then redirects to the login page in that request. Is there a way to configure the firewall to just give a different header and NOT redirect? Is that something I would need to handle in a ->before call? Or would a better way be to override the authentication entry point, as suggested in Symfony security return 401 response instead of redirect? That is for Symfony so not sure if Silex has some better sugar.
Tried this and other things:
$app->register(new SecurityServiceProvider(), array(
'security.firewalls' => array(
'default' => array(
'pattern' => '^/',
'anonymous' => true,
'oauth' => array(
'failure_path' => '/login',
'with_csrf' => true
),
'logout' => array(
'logout_path' => '/logout',
'with_csrf' => true
),
)
),
));
$app['security.authentication.entry_point.default.oauth'] = $app->share(function ($app) {
return new AuthenticationEntryPointComponent();
});
$app['security.authentication.entry_point.default.form'] = $app->share(function ($app) {
return new AuthenticationEntryPointComponent();
});
default is the name of the key in security.firewalls. Still getting the 302.
I had to dig around so much code but got it:
My controller:
$app['security.entry_point.form._proto'] =$app->protect(function ($name, array $options) use ($app) {
return $app->share(function () use ($app, $options) {
$login_path = isset($options['login_path']) ? $options['login_path'] : '/login';
return new MyCustomClass($app['security.http_utils'], $login_path);
});
});
The class:
namespace Yours;
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\HttpUtils;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class MyCustomClass implements AuthenticationEntryPointInterface {
protected $_login_path = '/login';
protected $_http_tools;
/**
* #param string $loginPath The path to the login form
*/
public function __construct(HttpUtils $http_utils, $login_path = '') {
$this->_http_tools = $http_utils;
if ( $login_path ) {
$this->_login_path = $login_path;
}
}
/**
* {#inheritDoc}
*/
public function start(Request $request, AuthenticationException $authException = null) {
if ($request->isXmlHttpRequest()) {
$response = new Response(json_encode([]), 401);
$response->headers->set('Content-Type', 'application/json');
return $response;
}
return $this->_http_tools->createRedirectResponse($request, $this->_login_path);
}
}
I was also facing this problem and was surprised that SecurityServiceProvider does not support entry_point option in firewal's configuration (just like Symfony security configuration does).
I've resolved it in different way. Instead of overriding security.entry_point.form._proto I followed SecurityServiceProviderbuilt-in authentication factories and in my custom service provider's register() I wrote like this:
$app['security.authentication_listener.factory.MY_CUSTOM_FIREWALL'] = $app->protect(function ($name, array $options) use ($app) {
$app['security.authentication_provider.'.$name.'.anonymous'] = $app['security.authentication_provider.anonymous._proto']($name);
$app['security.authentication_listener.'.$name.'.anonymous'] = $app['security.authentication_listener.anonymous._proto']($name, $options);
$app['security.entry_point.'.$name.'.MY_CUSTOM_FIREWALL'] = $app->share(function() use ($app) {
return new MyCustomFirewallEntryPoint($app['url_generator']);
});
return [
'security.authentication_provider.'.$name.'.anonymous',
'security.authentication_listener.'.$name.'.anonymous',
'security.entry_point.'.$name.'.MY_CUSTOM_FIREWALL',
'anonymous'
];
});
and then in security config:
$app->register(new Silex\Provider\SecurityServiceProvider(), [
'security.firewalls' => [
// All other routes require valid user
'secured' => [
'pattern' => '^.*$',
'anonymous' => true,
// It will search for custom factory
'MY_CUSTOM_FIREWALL' => true
],
],
]);

Categories