How can I intercept exceptions in ZendFramework 3 - php

I'm using ZendFramework 3 in my REST API project. So there are few modules and a plugin which checks an authorization status. If the authorization fails it throws an Exception.
There is no way to handle it in each controller separately using try .. catch. How can I intercept and handle the Exception and generate JSON output like this?
{
message: "Access denied",
reason: "Your token is incorrect"
}
I'm a newbie in ZendFramework, that's why I have no idea how to do this. And official documentation didn't say a word about this.

There are default framework Events that are triggered including the event MvcEvent::EVENT_DISPATCH_ERROR. So, all you should do is to attach listener on that error event and return JSON response.
First, you need to register your Listener in module.config.php
// In my case module name is Api
'listeners' => [
Api\Listener\ApiListener::class // Register the class listener
],
'service_manager' => [
'invokables' => [
// Register the class (of course you can use Factory)
Api\Listener\ApiListener::class => Api\Listener\ApiListener::class
],
],
Second, create the file class Api/Listener/ApiListener.php
<?php
namespace Api\Listener;
use Zend\EventManager\AbstractListenerAggregate;
use Zend\EventManager\EventManagerInterface;
use Zend\Mvc\MvcEvent;
use Zend\Console\Request as ConsoleRequest;
use Zend\View\Model\JsonModel;
class ApiListener extends AbstractListenerAggregate
{
public function attach(EventManagerInterface $events, $priority = 1)
{
// Registr the method which will be triggered on error
$this->listeners[] = $events->attach(MvcEvent::EVENT_DISPATCH_ERROR,
[$this, 'handleError'], 0);
}
/**
* Return JSON error on API URI(s)
*/
public function handleError(MvcEvent $e)
{
$request = $e->getParam('application')->getRequest();
if($request instanceof ConsoleRequest){
return;
}
//If you want to convert Response only on some URIs
//$uri = $request->getUri()->getPath();
//if(0 !== strpos($uri, '/api')){
// return;
//}
$response = $e->getResponse();
$exception = $e->getResult()->exception;
$errorType = $e->getError();
$errorCode = $exception && $exception->getCode() ? $exception->getCode() : 500;
$errorMsg = $exception ? $exception->getMessage() : $errorType;
$json = new JsonModel(['message' => $errorMsg]);
$json->setTerminal(true);
$response->setStatusCode($errorCode);
$e->setResult($json);
$e->setViewModel($json);
}
}
That's all. Now on every error, your custom logics will be executed.

Related

How I can hide some parameters from request body once an error is reported into sentry?

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.

Replacement for notFoundHandler setting

I'm migrating from Slim/3 to Slim/4. I've found or figured out replacements for all the features I was using that have been removed, except 404 Not Found Handler (part of the now gone App::$settings):
Slim App::$settings have been removed, multiple middleware have been implemented to replace the functionality from each individual settings.
Is there a middleware for notFoundHandler? If there isn't, how can I implement it?
Mine used to look like this:
use Slim\Container;
$config = new Container();
$config['notFoundHandler'] = function (Container $c) {
return function (Request $request, Response $response) use ($c): Response {
$page = new Alvaro\Pages\Error($c);
return $page->notFound404($request, $response);
};
};
According to Slim 4 documentation on error handling
Each Slim Framework application has an error handler that receives all
uncaught PHP exceptions
You can set a custom error handler to handle each type of exceptions thrown. A list of predefined exception classes is available on same page.
Here is a very basic example of how to register a closure as an error handler, to handle only HttpNotFoundException exceptions. You can also put the handler in a class that extends Slim\Handlers\ErrorHandler. Also, I did not actually use your Alvaro\Pages\Error to generate the response, but changing it should be straightforward:
<?php
require '../vendor/autoload.php';
$app = Slim\Factory\AppFactory::create();
// Define Custom Error Handler
$customErrorHandler = function (
Psr\Http\Message\ServerRequestInterface $request,
\Throwable $exception,
bool $displayErrorDetails,
bool $logErrors,
bool $logErrorDetails
) use ($app) {
$response = $app->getResponseFactory()->createResponse();
// seems the followin can be replaced by your custom response
// $page = new Alvaro\Pages\Error($c);
// return $page->notFound404($request, $response);
$response->getBody()->write('not found');
return $response->withStatus(404);
};
// Add Error Middleware
$errorMiddleware = $app->addErrorMiddleware(true, true, true);
// Register the handler to handle only HttpNotFoundException
// Changing the first parameter registers the error handler for other types of exceptions
$errorMiddleware->setErrorHandler(Slim\Exception\HttpNotFoundException::class, $customErrorHandler);
$app->get('/', function ($request, $response) {
$response->getBody()->write('Hello Slim 4');
return $response;
});
$app->run();
Another approach is to create a generic error handler and register it as the default handler, and inside that handler, decide what response should be sent based on type of exception that is thrown. Something like:
$customErrorHandler = function (
Psr\Http\Message\ServerRequestInterface $request,
\Throwable $exception,
bool $displayErrorDetails,
bool $logErrors,
bool $logErrorDetails
) use ($app) {
$response = $app->getResponseFactory()->createResponse();
if ($exception instanceof HttpNotFoundException) {
$message = 'not found';
$code = 404;
} elseif ($exception instanceof HttpMethodNotAllowedException) {
$message = 'not allowed';
$code = 403;
}
// ...other status codes, messages, or generally other responses for other types of exceptions
$response->getBody()->write($message);
return $response->withStatus($code);
};
Then you can set this as the default error handler:
$errorMiddleware = $app->addErrorMiddleware(true, true, true);
$errorMiddleware->setDefaultErrorHandler($customErrorHandler);

Laravel custom exception

Before posting this question I have searched internet for appropriate answers but got none.These are my following questions:
1) How to throw exception without try catch in laravel controller and get exception in the view of the called controller.
Example : TestController.php
function index(){
throw new CustomException('Data not found');
return view('dashboard');
}
How to get exception message in dashboard view
2) How to set format for exception message, suppose I want to return format as
$response['code'] = 0;
$response['status'] = 'error';
$response['message'] = 'message';
$response['data'] = '';
I have created a custom exception but don't know how to use it to fully
<?php
namespace App\Exceptions;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Mockery\Exception;
class CustomException extends Exception{
public $response;
/**
* Report the exception.
*
* #return void
*/
public function report()
{
}
/**
* Render the exception into an HTTP response.
*
* #param \Illuminate\Http\Request
* #return \Illuminate\Http\Response
*/
public function render($request,$exception){
ob_clean();
$response['code'] = 0;
$response['status'] = 'error';
$response['message'] = 'Message';
$response['data'] = '';
if(!$request->ajax()){
// non ajax response
}else{
return response()->json($response);
}
}
}
Answering your questions:
To pass this exception to view, you can implement render method what you already started to do. You can just do:
if(!$request->ajax()){
view('error_handler', compact('exception'))
} else {
return response()->json($response);
}
so now you can just create error_handler.blade.php view and you will have access in there to $exception variable, so you can use in there {{ $exception->getMessage}} and so on
You didn't define what exactly you want to achieve, however it should work without any problem:
public function render($request,$exception) {
if(!$request->ajax()){
view('error_handler', compact('exception'))
}
return response()->json([
'code' => $exception->getCode(),
'status' => 'error',
'message' => $exception->getMessage(),
'data' => 'sample data'
]);
}
Of course instead of using $exception->getCode() for code you can put anything you want, this is just an example that you can also use code and message that you have in your exception assuming you set some custom when you throw exception for example:
throw new CustomException('This is custom message', 123);
There isn't realy reason to thrown an exception if your purpose is to show the message of that exception within the view which is rendered by the controller. And It isn't a best way to manage exception, because by default all exception which are throw are handle and catch within the App\Exceptions\Handler class.
I think you know exactly when that type of CustomExption you have create will be thrown, but instead of throwing that error just traite that error which need an exception in different way without exception. For that you can create an Array in which old code, status, message, data and pass it to the view method;
class CustomController extends Controller
{
public function samemethod(){
// some code that can generate an error
// construct the error data
$customErrorData = [
'code' => 0000,
'status' => "some status",
'message' => "Some error message",
'data' => [...]
];
// After error data creation you can pass it to the view
return View::make('customview', compact('customErrorData'));
}
}
You have you error data in your view
All uncaught exceptions are intercepted by default exceptions handler. If you want this to behave differently, you just need to modify it: https://laravel.com/docs/5.7/errors#the-exception-handler

Symfony: How to return a JSON response from a Before Filter (Listener)?

From following this example I have managed to set up the below Listener/Before Filter to parse all requests to my API endpoint (ie. /api/v1) before any controller actions are processed. This is used to validate the request type and to throw an error if certain conditions are not met.
I would like to differentiate the error response based on the applications environment state. If we are in development or testing, I simply want to throw the error encountered. Alternatively, if in production mode I want to return the error as a JSON response. Is this possible? If not, how could I go about something similar?
I'm using Symfony v3.1.*, if that makes any difference.
namespace AppBundle\EventListener;
use AppBundle\Interfaces\ApiAuthenticatedController;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
class ApiBeforeListener
{
/**
* #var \Symfony\Component\DependencyInjection\ContainerInterface
*/
protected $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
/**
* Validates a request for API resources before serving content
*
* #param \Symfony\Component\HttpKernel\Event\FilterControllerEvent $event
* #return mixed
*/
public function onKernelController(FilterControllerEvent $event)
{
$controller = $event->getController();
if (!is_array($controller))
return;
if ($controller[0] instanceof ApiAuthenticatedController) {
$headers = $event->getRequest()->headers->all();
// only accept ajax requests
if(!isset($headers['x-requested-with'][0]) || strtolower($headers['x-requested-with'][0]) != 'xmlhttprequest') {
$error = new AccessDeniedHttpException('Unsupported request type.');
if (in_array($this->container->getParameter("kernel.environment"), ['dev', 'test'], true)) {
throw $error;
} else {
// return json response here for production environment
//
// For example:
//
// header('Content-Type: application/json');
//
// return json_encode([
// 'code' => $error->getCode(),
// 'status' => 'error',
// 'message' => $error->getMessage()
// ]);
}
}
}
}
}
Unlike most events, the FilterControllerEvent does not allow you to return a response object. Be nice if it did but oh well.
You have two basic choices.
The best one is to simply throw an exception and then add an exception listener. The exception listener can then return a JsonResponse based on the environment.
Another possibility to to create a controller which only returns your JsonResponse then use $event->setController($jsonErrorController) to point to it.
But I think throwing an exception is probably your best bet.
More details here: http://symfony.com/doc/current/reference/events.html

Symfony2 Deny User Login Based on Custom Status

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

Categories