CakePHP 3.x catch selected controller from logging if MissingControllerException occurs - cakephp

In CakePHP 3.x how can I catch selected controller names from logging in error.log file if MissingControllerException occurs.
Currently I am having loggs e.g.:
2022-02-17 09:32:34 Error: [Cake\Routing\Exception\MissingControllerException] Controller class AAA could not be found.
I tried to write own MyExceptionRenderer.php in src/Error folder:
namespace App\Error;
use Cake\Error\ExceptionRenderer;
class MyExceptionRenderer extends ExceptionRenderer{
public function missingController($error){
// Logic if AAA occuress do sth, e.g. redirect to selected page
}
}
In app.php I added:
'Error' => [
'errorLevel' => E_ALL & ~E_DEPRECATED,
'exceptionRenderer' => 'App\Error\MyExceptionRenderer',
'skipLog' => [],
'log' => true,
'trace' => true,
],
But it does not work

As mentioned in the comments, I don't see anything wrong with shown code, I've tried it 1:1 and it works fine for me using CakePHP 3.10.2, the missingController() method is being invoked as expected.
If your renderer is being used, but the method is not being invoked, then you'll have to debug the core exception renderer that you're extending, and figure out what exactly is happening when \Cake\Error\ExceptionRenderer::_customMethod() should be invoked.
That being said, I'd probably use a custom middleware for this instead, as you're not actually going to do exception rendering. Put it after the error handler middleware and catch the exceptions that you want to catch, something along the lines of this:
// in src/Middleware/MissingControllerHandlerMiddleware.php
namespace App\Middleware;
use Cake\Routing\Exception\MissingControllerException;
use Cake\Utility\Hash;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
class MissingControllerHandlerMiddleware
{
/**
* Invoke method.
*
* #param \Psr\Http\Message\ServerRequestInterface $request The request.
* #param \Psr\Http\Message\ResponseInterface $response The response.
* #param callable $next Callback to invoke the next middleware.
* #return \Psr\Http\Message\ResponseInterface A response
*/
public function __invoke(ServerRequestInterface $request, ResponseInterface $response, $next)
{
try {
return $next($request, $response);
} catch (MissingControllerException $exception) {
$class = Hash::get($exception->getAttributes(), 'class');
if ($class === 'AAA') {
return $response->withHeader('Location', 'https://example.com/missing-aaa-controller-redirect');
}
if ($class === 'BBB') {
// ...
}
// ...
throw $exception;
}
}
}
// in src/Application.php
public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
{
$middlewareQueue
->add(new ErrorHandlerMiddleware(Configure::read('Error')))
->add(new \App\Middleware\MissingControllerHandlerMiddleware())
// ...
return $middlewareQueue;
}
See also
Cookbook > Middleware > Creating Middleware

Related

Symfony: How to load all paths from the same Index route (to use dynamic routing in a React SPA)

I'm creating a SPA backed by Symfony and ApiPlatform so I want to always load my main route despite the real path of the URL.
I want something like this:
/**
* {#inheritdoc}
*/
class DefaultController extends Controller
{
/**
* #Route("/*", name="homepage")
*
* #return Response
*/
public function indexAction(): Response
{
// replace this example code with whatever you need
return $this->render('default/index.html.twig');
}
}
In my intentions, also if the URL is something like /path/to/the/spa/page I want to anyway load the DefaultController::indexAction()route.
How to do this? (obviously the provided example doesn't work).
Ok, I've found the solution after an "illumination".
I remembered that there is the possibility to rewrite all URL adding or removing the trailing slash
Reading that article I saw this:
class RedirectingController extends Controller
{
/**
* #Route("/{url}", name="remove_trailing_slash",
* requirements={"url" = ".*\/$"})
*/
public function removeTrailingSlash(Request $request)
{
// ...
}
}
So, to intercept all URL despite the path, my DefaultController::indexAction() becomes this:
class DefaultController extends Controller
{
/**
* #Route("/{url}",requirements={"url"=".*"}, name="homepage")
*
* #return Response
*/
public function indexAction(): Response
{
// replace this example code with whatever you need
return $this->render('default/index.html.twig');
}
}
Now all URL are all handled by DefaultController::indexAction() despite the URL path.
I would recommend you to use symfony's event system instead.
Subscribe to either kernel.request or kernel.router events.
In case of kernel.request you have to overtake the Symfony\Component\HttpKernel\EventListener\RouterListener::onKernelRequest()
which priotiry is 32 (use 33 at least).
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
class SpaSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return [
KernelEvents::REQUEST => ['onKernelRequest', 33],
];
}
public function onKernelRequest(RequestEvent $event)
{
$request = $event->getRequest();
if (!$request->isXmlHttpRequest()) {
$html = $this->twig->render('spa.html.twig', [
'uri' => $request->getUri(),
]);
$response = new Response($html, Response::HTTP_OK);
$event->setResponse($response);
}
}
}
In case of kernel.router use priority 1 at least.
You can use the php bin/console debug:event-dispatcher command to find out which listeners are registered for events and their priorities.

Laravel 5 Password Reset with Angular View

I am trying to use the Laravel inbuilt password reset in my app where Laravel 5.1 acts as the backend api and Angular 1.3 for all front-end views. I have set-up the Password reset as per the docs where I have done the following:
1) Create the table
php artisan migrate
2) Added this to the route:
Route::post('password/email', 'Auth/PasswordController#postEmail');
Route::post('password/reset', 'Auth/PasswordController#postReset');
Since I will be using Angular to display frontend forms, I did not add the views for GET. I havent done any changes to the Auth/PasswordController.php and right now its just like the way it came. But when I test the above URL from Postman POST request, I am getting the error:
View [emails.password] not found.
How can I let Angular Handle the views and not have Laravel worry about the view? Do I have to have Laravel View for the inbuilt password reset to work? How do I approach this?
Override the postEmail and postReset methods so that they return a JSON response (don't let it redirect). Subsequently post to /password/email and /password/reset from Angular via xhr.
Open app/Http/Controllers/Auth/PasswordController.php
<?php namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
class PasswordController extends Controller
{
use ResetsPasswords;
//add and modify this methods as you wish:
/**
* Send a reset link to the given user.
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Http\Response
*/
public function postEmail(Request $request)
{
$this->validate($request, ['email' => 'required|email']);
$response = Password::sendResetLink($request->only('email'), function (Message $message) {
$message->subject($this->getEmailSubject());
});
switch ($response) {
case Password::RESET_LINK_SENT:
return redirect()->back()->with('status', trans($response));
case Password::INVALID_USER:
return redirect()->back()->withErrors(['email' => trans($response)]);
}
}
/**
* Reset the given user's password.
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Http\Response
*/
public function postReset(Request $request)
{
$this->validate($request, [
'token' => 'required',
'email' => 'required|email',
'password' => 'required|confirmed',
]);
$credentials = $request->only(
'email', 'password', 'password_confirmation', 'token'
);
$response = Password::reset($credentials, function ($user, $password) {
$this->resetPassword($user, $password);
});
switch ($response) {
case Password::PASSWORD_RESET:
return redirect($this->redirectPath());
default:
return redirect()->back()
->withInput($request->only('email'))
->withErrors(['email' => trans($response)]);
}
}
}
Ceckout your path to views folder in app\bootstrap\cache\config.php at section "view"
'view' =>
array (
'paths' =>
array (
0 => '/home/vagrant/Code/app/resources/views',
),
'compiled' => '/home/vagrant/Code/app/storage/framework/views',
),
this path MUST be at SERVER! not at you local mashine like
"D:\WebServers\home\Laravel\app\bootstrap\cache", if you use the homestead.
And You must use command like: "php artisan config:clear | cache" at SERVER!
I had the same problem than you. You could manage to change the view in config/auth.php if you have another one not in resources/views/emails/password.blade.php.
Because this view isn't created by default, that's why you got the error.

changing ZF2 form behavior when retrieving form

I'm wondering if there is a way to either pass additional parameters to the constructor (preferred) or retrieve the Request object to check the headers from within the Form constructor so that when I do a getForm in a controller, the form will be customized depending on how it is called?
I'm working on integrating AngularJs bindings and model tags into my form elements but I will need to modify how the submit button works whenever a form is called from Ajax vs being pulled into a Zend template via the framework.
Thus I would like to throw conditional parameters around where the submit button is added to the form, but I need to know if the rendered form is being viewed in zend or is being sent via an ajax call. I can detect the ajax call in the controller by looking at the request headers with isXmlHttpRequest(), but I'm not sure how to let the form know what the controller saw when it's retrieving the form with $this->getForm()
You can inject any options you like using a factory class.
use MyModule\Form\MyForm;
use Zend\ServiceManager\ServiceLocatorInterface;
use Zend\ServiceManager\FactoryInterface;
class MyFormFactory implements FactoryInterface
{
public function createService(ServiceLocatorInterface $formElementManager)
{
$serviceManager = $formElementManager->getServiceLocator();
$request = $serviceManager->get('Request');
// I would recommend assigning the data
// from the request to the options array
$options = [
'is_ajax' => $request->isXmlHttpRequest(),
];
// Although you could also pass in the request instance into the form
return new MyForm('my_form', $options, $request);
}
}
If you inject the request you will need modify MyForm::__construct.
namespace MyModule\Form;
use Zend\Form\Form;
use Zend\Http\Request as HttpRequest;
class MyForm extends Form
{
protected $request;
public function __construct($name, $options, HttpRequest $request)
{
$this->request = $request;
parent::__construct($name, $options);
}
}
Update your module.config.php to use the factory
return [
'form_elements' => [
'factories' => [
'MyModule\Form\MyForm' => 'MyModule\Form\MyFormFactory'
]
]
]
Then ensure you request the form from the service manager (in a controller factory)
$myForm = $serviceManager->get('FormElementManager')->get('MyModule\Form\MyForm');
My AbstractForm with helper functions (I just added the getRequest() to the bottom). Of course in a wider scale application I'd probably add error checking to make sure these were not called from the constructor (when the service manager would not yet be available)
namespace Application\Form;
use Zend\Form\Form as ZendForm;
use Zend\Http\Request as HttpRequest;
use Zend\Stdlib\RequestInterface as Request;
use Zend\Form\FormElementManager as ZendFormElementManager;
use Zend\ServiceManager\ServiceLocatorAwareInterface as ZendServiceLocatorAwareInterface;
use Zend\ServiceManager\ServiceLocatorInterface as ZendServiceLocatorInterface;
use Doctrine\ORM\EntityManager as DoctrineEntityManager
class AbstractForm extends ZendForm implements ZendServiceLocatorAwareInterface {
/**
* #var Request
*/
protected $request;
/**
* in form context this turns out to be Zend\Form\FormElementManager
*
* #var ZendFormElementManager $service_manager
*/
protected static $service_manager;
/**
* #var DoctrineEntityManager $entity_manager
*/
protected $entity_manager;
/**
* #var ZendServiceLocatorInterface $service_locator_interface
*/
protected $service_locator_interface;
public function __construct($name = null)
{
parent::__construct($name);
}
/**
* in form context this turns out to be Zend\Form\FormElementManager
*
* #param ZendFormElementManager $serviceLocator
*/
public function setServiceLocator(FormElementManager $serviceLocator)
{
self::$service_manager = $serviceLocator;
}
/**
* in form context this turns out to be Zend\Form\FormElementManager
*
* #return ZendFormElementManager
*/
public function getServiceLocator()
{
return self::$service_manager;
}
/**
* wrapper for getServiceLocator
* #return ZendFormElementManager
*/
protected function getFormElementManager() {
return $this->getServiceLocator();
}
/**
* this returns an actual service aware interface
*
* #return ZendServiceLocatorInterface
*/
protected function getServiceManager() {
if(!($this->service_locator_interface instanceof ZendServiceLocatorInterface)) {
$this->service_locator_interface = $this->getFormElementManager()->getServiceLocator();
}
return $this->service_locator_interface;
}
/**
* #return DoctrineEntityManager
*/
protected function getEntityManager() {
if(!($this->entity_manager instanceof \DoctrineEntityManager)) {
$this->entity_manager = $this->getServiceLocator()->getServiceLocator()->get('Doctrine\ORM\EntityManager');
}
return $this->entity_manager;
}
/**
* Get request object
*
* #return Request
*/
public function getRequest()
{
if (!$this->request) {
$this->request = $this->getServiceManager()->get('Request');
}
return $this->request;
}
}

CakePHP error log - can I exclude 404 errors?

The error log of my CakePHP app is full of 404 errors. Can I exclude these MissingControllerExceptions from appearing in the error log? Using Cake 2.3.
versions above 2.4 (as mentioned by null) have an undocumented feature for this purpose.
just modify your exception config:
Configure::write('Exception', array(
'handler' => 'ErrorHandler::handleException',
'renderer' => 'ExceptionRenderer',
'log' => true,
'skipLog'=>array(
'MissingControllerException'
)
));
skipLog takes names of exception classes to be excluded from log.
Simply redirecting or removing these URLs is not going to cut it.
A busy site will get hit by hundreds of "random" 404s every day, most from far east countries checking for exploits or URLs such as "/wp-admin".
Logging these with a full stack trace is completely unnecessary
Solution
You can override the default error handler in CakePHP, and log to app/tmp/logs/404.log instead.
In your app/Config/core.php file, define the class you want to handle exceptions:
Configure::write('Error', array(
'handler' => 'MyCustomErrorHandler::handleError',
'level' => E_ALL & ~E_DEPRECATED,
'trace' => true
));
Create the class within app/Lib/Error and include using App::uses in your app/Config/bootstrap.php file:
App::uses('MyCustomErrorHandler', 'Lib/Error');
Make an exact copy of the original ErrorHandler class, just change the class name, and somewhere within the handleException method check which exception is being thrown, and log somewhere else. It will look a bit like this;
App::uses('ErrorHandler', 'Error');
class MyCustomErrorHandler {
public static function handleException(Exception $exception) {
// some code...
if (in_array(get_class($exception), array('MissingControllerException', 'MissingActionException', 'PrivateActionException', 'NotFoundException'))) {
$log = '404';
$message = sprintf("[%s]", get_class($exception));
}
// more code...
}
}
Building on robmcvey's solution, here is what I had to do for CakePHP 2.6.
Configuration
In app/Config/core.php update the Exception configuration:
Configure::write('Exception', array(
'handler' => 'AppErrorHandler::handleException',
'renderer' => 'ExceptionRenderer',
'log' => true
));
In app/Config/bootstrap.php add a CakeLog configuration:
CakeLog::config('not_found', array(
'engine' => 'FileLog',
'types' => array('404'),
'file' => '404',
));
The type of 404 is the logging level it will listen in on for anything being written to logs e.g. CakeLog::write('404', 'That was not found.');
Extend ErrorHandler
Create the file app/Lib/Error/AppErrorHandler.php. Here we will extend Cake's ErrorHandler, overwritting three methods; handleException(), _getMessage(), and _log().
<?php
class AppErrorHandler extends ErrorHandler {
/**
* List of Cake Exception classes to record to specified log level.
*
* #var array
*/
protected static $_exceptionClasses = array(
'MissingControllerException' => '404',
'MissingActionException' => '404',
'PrivateActionException' => '404',
'NotFoundException' => '404'
);
public static function handleException(Exception $exception) {
$config = Configure::read('Exception');
self::_log($exception, $config);
$renderer = isset($config['renderer']) ? $config['renderer'] : 'ExceptionRenderer';
if ($renderer !== 'ExceptionRenderer') {
list($plugin, $renderer) = pluginSplit($renderer, true);
App::uses($renderer, $plugin . 'Error');
}
try {
$error = new $renderer($exception);
$error->render();
} catch (Exception $e) {
set_error_handler(Configure::read('Error.handler')); // Should be using configured ErrorHandler
Configure::write('Error.trace', false); // trace is useless here since it's internal
$message = sprintf("[%s] %s\n%s", // Keeping same message format
get_class($e),
$e->getMessage(),
$e->getTraceAsString()
);
self::$_bailExceptionRendering = true;
trigger_error($message, E_USER_ERROR);
}
}
/**
* Generates a formatted error message
*
* #param Exception $exception Exception instance
* #return string Formatted message
*/
protected static function _getMessage($exception) {
$message = '';
if (php_sapi_name() !== 'cli') {
$request = Router::getRequest();
if ($request) {
$message .= $request->here() . " Not Found";
}
}
$message .= "\nStack Trace:\n" . $exception->getTraceAsString() . "\n";
return $message;
}
/**
* Handles exception logging
*
* #param Exception $exception The exception to render.
* #param array $config An array of configuration for logging.
* #return bool
*/
protected static function _log(Exception $exception, $config) {
if (!empty(self::$_exceptionClasses)) {
foreach ((array)self::$_exceptionClasses as $class => $level) {
if ($exception instanceof $class) {
return CakeLog::write($level, self::_getMessage($exception));
}
}
}
return parent::_log();
}
}
You can customize the $_exceptionClasses array to catch which exceptions you want and send them to different logs. The _getMessage() method has been simplified to remove the attributes.
Result
Random URLs like /exploitable-plugin will now log to tmp/logs/404.log.
2015-04-01 16:37:54 404: /exploitable-plugin Not Found
Stack Trace:
#0 /var/example.com/app/index.php(146): Dispatcher->dispatch(Object(CakeRequest), Object(CakeResponse))
#1 {main}
I assume that your viewers hit those pages from results of search engines. Since you're a webmaster of this page, you could try to use something like Google removal tools https://www.google.com/webmasters/tools/removals . So there won't be any more liks to these pages. It is a little costly in time, but I don't see a way to exclude missing controller actions form logging.
Anyway here you have everything on logging in CakePHP: https://www.google.com/webmasters/tools/removals?pli=1
Edit:
Actually I'd found a way, but it might make you trouble in the future if you forgot about it. Add this to the end of bootstrap.php :
$matches = array();
preg_match('/^\/(.+?)\//', $_SERVER['REQUEST_URI'],$matches);
if(!App::import('Controller', $matches[1])){
die('404 Error![some_random_chars_so_you_can_easy_find_it_in_the_future]') ;
}

Cakephp - how to make error pages have its own layouts?

I wanna have a different layout for the page not found 404 page. How can i set a different layout for that page?
Savant from the IRC helped me out and he suggest in using beforeRender(){} in the app_controller
// Before Render
function beforeRender() {
if($this->name == 'CakeError') {
//$this->layout = 'error';
}
}
CakeError is a catchAll for errors :D
In CakePHP 2.2.2 I changed the ExceptionRenderer in core.php with my own, like this:
app/Config/core.php:
Configure::write('Exception', array(
'handler' => 'ErrorHandler::handleException',
'renderer' => 'MyExceptionRenderer', // this is ExceptionRenderer by default
'log' => true
));
app/Lib/Error/MyExceptionRenderer.php:
App::uses('ExceptionRenderer', 'Error');
class MyExceptionRenderer extends ExceptionRenderer {
protected function _outputMessage($template) {
$this->controller->layout = 'error';
parent::_outputMessage($template);
}
}
Just you need to make layout changes in your error400.ctp file under /app/View/Errors/error400.ctp
Open that file and set layout by
<?php $this->layout=''; //set your layout here ?>
better to create an error.php file in your app folder
class AppError extends ErrorHandler {
function error404($params) {
$this->controller->layout = 'error';
parent::error404($params);
}
}
so you can avoid the if-testing at EVERY page render that savants' solution introduces
My solution for CakePHP 2.3
Change the ExceptionRenderer in core.php to use your own renderer.
app/Config/core.php:
Configure::write('Exception', array(
'handler' => 'ErrorHandler::handleException',
'renderer' => 'MyExceptionRenderer',
'log' => true
));
app/Lib/Error/MyExceptionRenderer.php:
App::uses('ExceptionRenderer', 'Error');
class MyExceptionRenderer extends ExceptionRenderer
{
/**
* Overrided, to always use a bare controller.
*
* #param Exception $exception The exception to get a controller for.
* #return Controller
*/
protected function _getController($exception) {
if (!$request = Router::getRequest(true)) {
$request = new CakeRequest();
}
$response = new CakeResponse(array('charset' => Configure::read('App.encoding')));
$controller = new Controller($request, $response);
$controller->viewPath = 'Errors';
$controller->layout = 'error';
return $controller;
}
}
The advantage to this approach is that it ensures any exceptions thrown from AppController don't cause an endless loop when rendering the exception. Forces a basic rendering of the exception message every time.
This simplest way I know of is to create this function in your AppController:
function appError($method, $messages)
{
}
You can then do whatever you want with the error, display it however you like, or not display it at all, send an email etc.. (I'm not sure if this method if still valid.)
There is also an option of creating app_error.php in your app root, with class AppError extends ErrorHandler in it, which enables you to override all kinds of errors. But I haven't done this yet, so I can't tell you more about it.
See cake/libs/error.php and cake/libs/object.php and of course The Book for more info.
Edit: Forgot to mention, once you caught the error, there's nothing preventing you to - for example - store the error in session, redirect to your "error handling controller", and then display it in your controller however you want.

Resources