So, I have a table that is auto-generated using DataTables. An action in my CakePHP grabs the data for that table, and formats it into JSON for datatables to use, this is the formatted JSON:
<?php
$data = array();
if (!empty($results)) {
foreach ($results as $result) {
$data[] = [
'name' => $result->name,
'cad' => $this->Number->currency($result->CAD, 'USD'),
'usd' => $this->Number->currency($result->USD, 'USD'),
'edit' => '<a href="' .
$this->Url->build(['controller' => 'Portfolios', 'action' => 'edit', $result->id]) .
'"><i class="fa fa-pencil"></i></a>',
'delete' => '<input type="checkbox" class="delete" value="' . $result->id . '">'
];
}
}
echo json_encode(compact('data'));
As you can see, I have a 'delete' option in there that outputs a checkbox with the value of the id of the corresponding element. When that checkbox is checked, a delete button is showing which sends this ajax request:
$('a#delete').on('click', function(e) {
e.preventDefault();
var checkedValues = [];
$('input.delete:checked').each(function() {
checkedValues.push($(this).val());
});
$.ajax({
url: $(this).attr('href'),
type: 'POST',
data: checkedValues
});
})
This ajax post goes to my controller action delete(). The problem I'm having is that I'm getting an error that states "Invalid Csrf Token". I know why this is happening, I'm submitting a form with Csrf protection on, that has no Csrf token added to it.
I can't figure out how to manually create a Csrf token for this situation (where the input values are generated after the page has loaded). Nor can I figure out how to disable Csrf protection. I read this, but the code is placed in the beforeFilter function, and as far as I understand it, that means it's run on every action, not just this one, and that's not what I want. Plus, to be completely honest, I would prefer a solution where I don't deactivate security functions.
Is there anyway to disable Csrf for this specific action, or is there a better way to do this?
read all about the CSRF component here
http://book.cakephp.org/3.0/en/controllers/components/csrf.html
you can disable for a specific action here:
http://book.cakephp.org/3.0/en/controllers/components/csrf.html#disabling-the-csrf-component-for-specific-actions
public function beforeFilter(Event $event) {
if (in_array($this->request->action, ['actions_you want to disable'])) {
$this->eventManager()->off($this->Csrf);
}
}
Above answer does not work in Cakephp 3.6 or later.
Cakephp add object of CsrfProtectionMiddleware in src/Application.php.
If you have to remove CSRF protection for specific controller or action then you can use following work around:
public function middleware($middlewareQueue)
{
$middlewareQueue = $middlewareQueue
// Catch any exceptions in the lower layers,
// and make an error page/response
->add(ErrorHandlerMiddleware::class)
// Handle plugin/theme assets like CakePHP normally does.
->add(AssetMiddleware::class)
// Add routing middleware.
// Routes collection cache enabled by default, to disable route caching
// pass null as cacheConfig, example: `new RoutingMiddleware($this)`
// you might want to disable this cache in case your routing is extremely simple
->add(new RoutingMiddleware($this, '_cake_routes_'));
/*
// Add csrf middleware.
$middlewareQueue->add(new CsrfProtectionMiddleware([
'httpOnly' => true
]));
*/
//CSRF has been removed for AbcQutes controller
if(strpos($_SERVER['REQUEST_URI'], 'abc-quotes')===false){
$middlewareQueue->add(new CsrfProtectionMiddleware([
'httpOnly' => true
]));
}
return $middlewareQueue;
}
So i needed a fix for cakephp 3.7 and using $_SERVER['REQUEST_URI'] is realllly not the way to go here. So here is how you are supposed to do it after reading through some documentation.
In src/Application.php add this function
public function routes($routes)
{
$options = ['httpOnly' => true];
$routes->registerMiddleware('csrf', new CsrfProtectionMiddleware($options));
parent::routes($routes);
}
Comment out the existing CsrfProtectionMiddleware
public function middleware($middlewareQueue)
{
...
// $middlewareQueue->add(new CsrfProtectionMiddleware([
// 'httpOnly' => true
// ]));
}
Open your config/routes.php add $routes->applyMiddleware('csrf'); where you do want it
Router::prefix('api', function ($routes)
{
$routes->connect('/', ['controller' => 'Pages', 'action' => 'index']);
$routes->fallbacks(DashedRoute::class);
});
Router::scope('/', function (RouteBuilder $routes)
{
$routes->applyMiddleware('csrf');
$routes->connect('/', ['controller' => 'Pages', 'action' => 'dashboard']);
$routes->fallbacks(DashedRoute::class);
});
Note that my api user now has no csrf protection while the basic calls do have it.
If you have more prefixes don't forgot to add the function there aswell.
in Application.php this worked for me....
$csrf = new CsrfProtectionMiddleware();
// Token check will be skipped when callback returns `true`.
$csrf->whitelistCallback(function ($request) {
// Skip token check for API URLs.
if ($request->getParam('controller') === 'Api') {
return true;
}
});
Related
So after login in isAuthorized method I'm trying to redirect user based on a condition. But problem is it's not redirecting. Below the code that I have tried.
protected function isAuthorized($LoginUser)
{
if ($this->getTable('Users')->hasCompany($LoginUser) == false){
$this->redirect(['controller'=>'Companies','action'=>'edit']);
dd("hello");
}
}
It's not redirecting and getting hello message. How can I redirect after login user to another page based on condition ?
As mentioned in the comments, the auth component's authorization objects are supposed to return a boolean, and depending on that, let the component do the unauthorized handling-
What you could do, is for example dynamically set the component's unauthorizedRedirect option (and probably also authError) from the controller's authorization handler for that specific case (I guess you'd also have to exclude the respective company controller's action from that check, as otherwise you'll end up in an infinite redirect loop):
if (!$this->getTable('Users')->hasCompany($LoginUser)) {
$message = __('You must provide company information in order to proceed.');
$url = \Cake\Routing\Router::url([
'controller' => 'Companies',
'action' => 'add'
]);
$this->Auth->setConfig([
'authError' => $message,
'unauthorizedRedirect' => $url,
]);
return false;
}
// ...
return true;
If you find yourself in a situation where there's no such possibility, brute forcing a redirect by throwing a \Cake\Http\Exception\RedirectException could be a solution too, even though it's ideally avoided, it's better than dying, as it will at least emit a clean redirect response:
$url = \Cake\Routing\Router::url([
'controller' => 'Companies',
'action' => 'add'
]);
throw new \Cake\Http\Exception\RedirectException($url);
See also
Cookbook > Controllers > Components > AuthComponent > Configuration options
I am using authentication plugin for login in cakephp.
application.php , configAuth() method code
protected function configAuth(): \Authentication\AuthenticationService
{
$authenticationService = new \Authentication\AuthenticationService([
// 'unauthenticatedRedirect' => '/cake_invo/users/login' , //<= manually working fine
'unauthenticatedRedirect' => \Cake\Routing\Router::url(['controller' => 'Users', 'action' => 'login']),
'queryParam' => 'redirect',
]);
// Load identifiers, ensure we check email and password fields
$authenticationService->loadIdentifier('Authentication.Password', [
'fields' => [
'username' => 'email',
'password' => 'password',
]
]);
// Load the authenticators, you want session first
$authenticationService->loadAuthenticator('Authentication.Session');
// Configure form data check to pick email and password
$authenticationService->loadAuthenticator('Authentication.Form', [
'fields' => [
'username' => 'email',
'password' => 'password',
],
// 'loginUrl' => '/cake_invo/users/login' //<= manually working fine
'loginUrl' => \Cake\Routing\Router::url(['controller' => 'Users', 'action' => 'login']),
]);
return $authenticationService;
}
middleware method
public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
{
$middlewareQueue
->add(new ErrorHandlerMiddleware(Configure::read('Error')))
->add(new AssetMiddleware([
'cacheTime' => Configure::read('Asset.cacheTime'),
]))
->add(new RoutingMiddleware($this))
// add Authentication after RoutingMiddleware
->add(new \Authentication\Middleware\AuthenticationMiddleware($this->configAuth()));
return $middlewareQueue;
}
Getting error like image
How can I solve this problem ?
After give command bin/cake routes
The problem is that your routes aren't loaded yet when you make the call to Router::url(), hence it will fail.
While the order of the middlewares is correct, ie the authentication middleware is being added after the routing middleware, you are building the authentication service immediately by invoking $this->configAuth(), which means that the Router::url() call will be invoked before any of the middlewares have run, specifically before the routing middleware has run, which is responsible for loading your routes.
Instead of passing a built authentication service instance, setup things as shown in the docs, that is make sure that your Application class implements \Authentication\AuthenticationServiceProviderInterface, change your configAuth method to match AuthenticationServiceProviderInterface::getAuthenticationService(), and then pass $this to the authentication middleware constructor instead. That way the method will only be invoked when the authentication middleware runs, that is after the routing middleware.
// ...
use Authentication\AuthenticationServiceInterface;
use Authentication\AuthenticationServiceProviderInterface;
use Cake\Http\BaseApplication;
use Psr\Http\Message\ServerRequestInterface;
class Application extends BaseApplication implements AuthenticationServiceProviderInterface
{
// ...
public function getAuthenticationService(ServerRequestInterface $request): AuthenticationServiceInterface
{
// ...
return $authenticationService;
}
public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
{
$middlewareQueue
// ...
->add(new RoutingMiddleware($this))
->add(new AuthenticationMiddleware($this));
return $middlewareQueue;
}
// ...
}
I have a cakephp plugin that creates image thumbnails.
Currently, thumbnails are "served" by an action of a controller that returns a file as response (here).
This is the route:
Router::plugin(THUMBER, ['path' => '/thumb'], function (RouteBuilder $routes) {
$routes->get('/:basename', ['controller' => 'Thumbs', 'action' => 'thumb'], 'thumb')
->setPatterns(['basename' => '[\w\d=]+'])
->setPass(['basename']);
});
So the url are these (the thumbnail basename is encoded):
/thumb/ZDc1NTYyMGY1N2VmMzRiNTQyZjE0NTY2Mjk0YWQ2NTFfNGIyYTBkMjVhMTdjZTdjN2E4MjVjY2M1YWU1ODNhMzcuZ2lm
Now I'm trying to replace the controller with a middleware.
This is quite simple, because basically it would work like the AssetMiddleware and the __invoke() method is almost like the old action method:
class ThumbnailMiddleware
{
use ThumbsPathTrait;
public function __invoke($request, $response, $next)
{
if ($request->getParam('_name') !== 'thumb' || !$request->getParam('basename')) {
return $next($request, $response);
}
$file = $this->getPath(base64_decode($request->getParam('basename')));
if (!is_readable($file)) {
throw new ThumbNotFoundException(__d('thumber', 'File `{0}` doesn\'t exist', $file));
}
$response = $response->withModified(filemtime($file));
if ($response->checkNotModified($request)) {
return $response;
}
return $response->withFile($file)->withType(mime_content_type($file));
}
}
This works very well.
The problem is that it now works because this middleware takes the controller's route and "intercepts" the request, "resolving" it without going through the controller (see the first three lines of the __invoke() method).
Now I would like to rewrite that route and untie it from the controller, which I should totally eliminate from the plugin.
Obviously it works well like this (the second parameter is null):
$routes->get('/:basename', null, 'thumb')
->setPatterns(['basename' => '[\w\d=]+'])
->setPass(['basename']);
Or I could just call the RouteBuilder::fallback() method and analyze the url of the request (as it happens for the AssetMiddleware).
But I was wondering if there is a way to link a route only to a middleware and explicitly for a middleware.
Or, if not, what is the best method.
I know that I can apply "middleware to specific routing scopes" (cookbook), so I'm wondering if this is definitely the correct formula:
Router::plugin(THUMBER, ['path' => '/thumb'], function (RouteBuilder $routes) {
$routes->registerMiddleware('thumbnail', new ThumbnailMiddleware);
$routes->applyMiddleware('thumbnail');
$routes->get('/:basename', null, 'thumb')
->setPatterns(['basename' => '[\w\d=]+'])
->setPass(['basename']);
});
Well, you are not required to pass any defaults to a route, so from that point of view it's correct, the route wouldn't be "bound" to a controller. If your middleware wouldn't intercept the request, then things would end up with a MissingControllerException being thrown, as the dispatcher would get null as the controller name. The resulting error message would probably be a bit misleading, as there would be no controller name to include.
What you are doing there will cause your middleware to apply to all routes in the /thumb scope, so if there would ever be any other routes, then your middleware would require according parameter checks. You can restrict things further by applying the middleware to that one specific route, instead of the route builder:
// $routes->applyMiddleware('thumbnail'); // don't do that
$routes
->get('/:basename', null, 'thumb')
->setPatterns(['basename' => '[\w\d=]+'])
->setPass(['basename'])
->setMiddleware(['thumbnail']); // do this instead
That way your middleware would only be invoked for that specific route.
I am trying to pass JSON into an Add() action of my controller but the response I get is either a response from the Index() action or "null" depending on entries in my routes.php (shown below).
I'm using Firefox plugin RESTClient to test/pass json to my actions:
Method: POST
URL: http://MyApp/notes.json (my understanding is this should call Add action based on cakephp docs )
JSON:
{
"post_id":"123",
"note":"hello world"
}
When I have the following in my routes.php, the response is "null"
Router::scope('/', function ($routes) {
$routes->extensions(['json']);
$routes->resources('Notes');
Removing $routes->resources('Notes'); from this scope returns a response from the index action and all items in the Notes model are returned in the response.
Furthermore, when I implement this through Crud.Add in an api prefix, I get valid results. That being said trying to understand what I have incorrect in my routes.php or add action that could be causing the null or routing to index action.
Add Action:
public function add()
{
$note = $this->Notes->newEntity();
if ($this->request->is('post')) {
$note = $this->Notes->patchEntity($note, $this->request->data);
if ($this->Notes->save($note)) {
$message = 'Saved';
} else {
$message = 'Error';
}
}
$this->set('_serialize', ['note', 'message']);
}
Routes.php:
<?php
use Cake\Core\Plugin;
use Cake\Routing\Router;
Router::defaultRouteClass('DashedRoute');
Router::extensions(['json'], ['xml']);
Router::prefix('api',function($routes) {
$routes->extensions(['json','xml']);
$routes->resources('Notes');
});
Router::scope('/', function ($routes) {
$routes->extensions(['json']);
$routes->resources('Notes');
$routes->connect('/', ['controller' => 'Pages', 'action' => 'display', 'home']);
$routes->connect('/pages/*', ['controller' => 'Pages', 'action' => 'display']);
$routes->fallbacks('DashedRoute');
});
Plugin::routes();
Thanks in advance.
You didn't set any variables for the view, other than serialization options, so you get null because there's nothing to serialize, ie you are missing something like
$this->set(compact('note', 'message'));
in your action.
And without the resource routes a request to /notes maps to the index action of the Notes controller, because that's how fallback routes are connected, no action = index action.
See also
Cookbook > Views > JSON And XML Views > Using Data Views with the Serialize Key
Cookbook > Routing > Fallbacks Method
Reports has many ReportInstances
ReportInstances belongs to Reports
I would like to have the url /reports/:report-id/instances to point to the action index_by_report_id inside ReportInstancesController.php
How do I configure the routes.php accordingly?
UPDATE:
I tried nested resources as described here:
http://book.cakephp.org/3.0/en/development/routing.html#creating-nested-resource-routes
Here are my routes
$routes->resources('Reports', [
'map' => [
'standard' => [
'action' => 'standard',
'method' => 'GET',
]
]
]);
$routes->resources('Reports', function ($routes) {
$routes->resources('ReportInstances');
});
When I do a /reports/1/instances, it goes to ReportsController looking for action 1.
Please advise.
Do this in your routes.php
$routes->resources('Parents', function ($routes) {
$routes->resources('Children');
});
$routes->resources('Children');
In ChildrenController.php,
protected function _prepareConditions() {
$parentId = isset($this->request->params['parent_id']) ? $this->request->params['parent_id'] : null;
if ($parentId == null) {
return [];
}
return [
'Children.parent_id' => $parentId
];
}
public function index()
{
$conditions = $this->_prepareConditions();
$this->paginate = [
'contain' => ['Parents'],
'conditions' => $conditions
];
// ... and so on
You will be able to do the following:
/parents/1/children
/parents/1/children.json
/children
/children.json
Why this works?
http://book.cakephp.org/3.0/en/development/routing.html#creating-nested-resource-routes
tells us that basically to basically retrieve the parent id from the request params.
What it does not say explicitly is that, the routes will then reuse the basic 5 functions: index, add, view, delete, edit even when you nest them under a parent url.
Why do you still have a separate resources route for the Children?
This allows the /children and /children.json to work if you need them as well.
What about add?
I haven't tried that but I do not foresee any issues with using that as
/parents/1/children/add
/parents/1/children/add.json
/children/add
/children/add.json