CakePHP REST Authorization issue on POST and PUT (I get 404) - cakephp

I'm developing a REST api for a application, and everething went fine up until now...
I'm building a header with login data, GET and DELETE work fine but when I try to send a PUT or POST request I get 404...
When authorization is off (i.e., I do not check it in cake) everything works fine.
Here's the controller code:
class SitesController extends AppController {
var $uses = array("Site");
var $name = 'Sites';
var $scaffold;
var $components = array('RequestHandler','Security');
function beforeFilter() {
$this->Security->loginOptions = array(
'type'=>'basic'
);
$this->Security->loginUsers = array(
'lukasz'=>'blabla',
'test'=>'test'
);
$this->Security->requireLogin();
}
function index() {
$sites = $this->Site->find('all');
$this->set(compact('sites'));
}
function view($id) {
$site = $this->Site->findById($id);
$this->set(compact('site'));
}
function add() {
if($this->data != null) {
$this->Site->create();
if($this->Site->save($this->data)) {
$message = array('Deleted');
} else {
$message = $this->data;
}
$this->set(compact("message"));
}
}
function edit($id) {
$this->Site->id = $id;
if ($this->Site->save($this->data)) {
$message = array('Saved');
} else {
$message = array('Error');
}
$this->set(compact("message"));
}
function delete($id) {
if($this->Site->delete($id)) {
$message = array('Deleted');
} else {
$message = array('Error');
}
$this->set(compact("message"));
}
}
And here's how I send requests:
http://bin.cakephp.org/view/165115685
http://bin.cakephp.org/view/1477117088

I suspect you're running into the CSRF protection (form spoofing protection) the SecurityComponent applies to all POST and PUT requests. Try turning it off using the $validatePost option.

Related

PUT request with formData could not update using react.js and laravel

I am not receiving formdata in the Api endpoint which is being sent by fetch and PUT method.
I used the POST method and it worked out which i think is not recommended for updating.
I have react running on localhost:3000 and the laravel-API running on localhost:5000.
This is the route in API
Route::put('updateSlide/{id}', 'SlidesController#updateSlide');
This is what is in the controller
public function updateImage(Request $request, int $id)
{
$image = $this->slideRepo->findSlideById($id)->image;
if ($image) {
$result = Storage::disk('ucmp')->delete($image);
}
if ($request->hasFile('image') && $request->file('image') instanceof UploadedFile) {
return $this->slideRepo->saveCover($request->file('image'));
} // return response()->json($data);
// data is an array (note)
return null;
}
public function updateSlide(Request $request, int $id)
{
$imageUrl=$this->updateImage($request, $id);
return response()->json($this->slideRepo->updateSlide([
'caption' => $request['caption'],
'image' => $imageUrl,
'url' => $request['url']
],$id));
}
This is the function sending to fetch
export const updateSlideApi = (token, _slide, id) => {
return {
url: `${BASE_URL}/api/updateSlide/${id}`,
opt: API.requestOptions("PUT",token,null,{ body: _slide }, true)
};
};
In my header i do not have the content-type.
I expect json data from the laravel API function but am having an error, " Can only throw objects"
PHP does not parse the body of a PUT request.
Use POST and add parameter _method with value PUT.
PHP put has given me tough time, use this function will save your time parsing it else method:
function parsePutRequest()
{
// Fetch content and determine boundary
$raw_data = file_get_contents('php://input');
$boundary = substr($raw_data, 0, strpos($raw_data, "\r\n"));
// Fetch each part
$parts = array_slice(explode($boundary, $raw_data), 1);
$data = array();
foreach ($parts as $part) {
// If this is the last part, break
if ($part == "--\r\n") break;
// Separate content from headers
$part = ltrim($part, "\r\n");
list($raw_headers, $body) = explode("\r\n\r\n", $part, 2);
// Parse the headers list
$raw_headers = explode("\r\n", $raw_headers);
$headers = array();
foreach ($raw_headers as $header) {
list($name, $value) = explode(':', $header);
$headers[strtolower($name)] = ltrim($value, ' ');
}
// Parse the Content-Disposition to get the field name, etc.
if (isset($headers['content-disposition'])) {
$filename = null;
preg_match(
'/^(.+); *name="([^"]+)"(; *filename="([^"]+)")?/',
$headers['content-disposition'],
$matches
);
list(, $type, $name) = $matches;
isset($matches[4]) and $filename = $matches[4];
// handle your fields here
switch ($name) {
// this is a file upload
case 'userfile':
file_put_contents($filename, $body);
break;
// default for all other files is to populate $data
default:
$data[$name] = substr($body, 0, strlen($body) - 2);
break;
}
}
}
return $data;
}

Can not config Header security middleware CakePHP

I have a site built with CakePHP, default it doesn't allow subdomain embed on iframe.
I configured Frame Option on nginx/conf.d and now my homepage can be embedded in an iframe of a subdomain.
However, another post cannot be embedded on a subdomain's iframe. (exp: http://example.net/postabc will not display). I tried to change options on Header security middleware(please read below).
Is there anywhere I need to change the configuration to so all my posts can be displayed?
<?php
namespace Cake\Http\Middleware;
use InvalidArgumentException;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
class SecurityHeadersMiddleware
{
protected $headers = [];
public function noSniff()
{
$this->headers['x-content-type-options'] = 'nosniff';
return $this;
}
public function noOpen()
{
$this->headers['x-download-options'] = 'noopen';
return $this;
}
public function setReferrerPolicy($policy = 'same-origin')
{
$available = [
'no-referrer', 'no-referrer-when-downgrade', 'origin',
'origin-when-cross-origin',
'same-origin', 'strict-origin', 'strict-origin-when-cross-origin',
'unsafe-url'
];
$this->checkValues($policy, $available);
$this->headers['referrer-policy'] = $policy;
return $this;
}
public function setXFrameOptions($option = 'allow-from', $url = 'http://subdomain.example.net')
{
$this->checkValues($option, ['deny', 'sameorigin', 'allow-from']);
if ($option === 'allow-from') {
if (empty($url)) {
throw new InvalidArgumentException('The 2nd arg $url can not be empty when `allow-from` is used');
}
$option .= ' ' . $url;
}
$this->headers['x-frame-options'] = $option;
return $this;
}
public function setXssProtection($mode = 'block')
{
$mode = (string)$mode;
if ($mode === 'block') {
$mode = '1; mode=block';
}
$this->checkValues($mode, ['1', '0', '1; mode=block']);
$this->headers['x-xss-protection'] = $mode;
return $this;
}
public function setCrossDomainPolicy($policy = 'all')
{
$this->checkValues($policy, ['all', 'none', 'master-only', 'by-content-type', 'by-ftp-filename']);
$this->headers['x-permitted-cross-domain-policies'] = $policy;
return $this;
}
protected function checkValues($value, array $allowed)
{
if (!in_array($value, $allowed)) {
throw new InvalidArgumentException(sprintf(
'Invalid arg `%s`, use one of these: %s',
$value,
implode(', ', $allowed)
));
}
}
public function __invoke(ServerRequestInterface $request, ResponseInterface $response, $next)
{
$response = $next($request, $response);
foreach ($this->headers as $header => $value) {
$response = $response->withHeader($header, $value);
}
return $response;
}
}

Using session component in custom component

I'm trying to use Session component in custom component (CakePHP 2.3) but when I call Session component functions I get: Fatal error: Call to a member function read() on a non-object in ...\app\Controller\Component\CartComponent.php on line 7
My CartComponent looks like that:
<?php
App::uses('Component', 'Controller');
class CartComponent extends Component {
public $components = array('Session');
function hasItems() {
$cart = $this->Session->read('Cart');
return $cart != null && count($cart) > 0;
}
}
?>
And I use it in controller:
<?php
class OrdersController extends AppController {
public $name = 'Orders';
public $components = array('Cart', 'Email');
function beforeFilter() {
parent::beforeFilter();
if ($this->Cart->hasItems()) {
$this->Auth->allow('add_item', 'remove_item', 'cart');
} else {
$this->Auth->allow('add_item', 'remove_item', 'cart', 'make');
}
}
}
?>
For using session inside the custom component I tried with
public $components = array('Session');
and then called it by using
$this->Session->read('Cart');
but I cant able to use it and I start to use
CakeSession::read('Cart')
Now it works Hope it will used for you note I used in cake php version > 2
If you want to use Session in your Component Use-
$test = CakeSession::read('user');
print_r($test);
You should use as bellow
class YourComponent extends Component {
public function initialize(Controller $controller){
$this->controller = $controller;
if (!isset($this->controller->presetVars)) {
$this->controller->presetVars = true;
}
$model = $this->controller->modelClass;
if (!empty($settings['model'])) {
$model = $settings['model'];
}
if ($this->controller->presetVars === true) {
// auto-set the presetVars based on search definitions in model
$this->controller->presetVars = array();
$filterArgs = array();
if (!empty($this->controller->$model->filterArgs)) {
$filterArgs = $this->controller->$model->filterArgs;
}
foreach ($filterArgs as $key => $arg) {
if ($args = $this->_parseFromModel($arg, $key)) {
$this->controller->presetVars[] = $args;
}
}
}
foreach ($this->controller->presetVars as $key => $field) {
if ($field === true) {
if (isset($this->controller->$model->filterArgs[$key])) {
$field = $this->_parseFromModel($this->controller->$model->filterArgs[$key], $key);
} else {
$field = array('type' => 'value');
}
}
if (!isset($field['field'])) {
$field['field'] = $key;
}
$this->controller->presetVars[$key] = $field;
}
/* now you can use Component existing in your Component :) */
public function sayHello(){
$this->controller->Session->setFlash(__('Hello you'));
}
}

update Auth session

How to update user information stored in auth session? without logout and login again.
I think this function will do it.. but is it the best-practice?
function update($field, $value){
$this->Session->write($this->Auth->sessionKey . '.' . $field, $value);
}
Yes.
You could grab the current info array, modify it, and then call $this->Auth->login($newUserData);, but this will also renew the session (no user interaction needed, though). Note: Applies to CakePHP 2.0+ only.
I've completed update function to get an array of new values. with keys (field name):
public function update($fields, $values = null) {
if (empty(parent::$_user) && !CakeSession::check(parent::$sessionKey)) {
return false;
}
if (!empty(parent::$_user)) {
$user = parent::$_user;
} else {
$user = CakeSession::read(parent::$sessionKey);
}
if (is_array($fields)) {
if (is_array($values)) {
$data = array_combine($fields, $values);
} else {
$data = $fields;
}
} else {
$data = array($fields => $values);
}
foreach ($data as $field => $value) {
if (isset($user[$field])) {
$user[$field] = $value;
}
}
return $this->login($user);
}
(thanks to tigrang for login function)

Opposite of requireSecure in CakePHP

CakePHP has a requireSecure function in the SecurityComponent. I'm using this to force SSL when passing sensitive information such as a credit card number.
Questions:
Is there a requireNonSecure function?
If there's no requireNonSecure function is it possible to extend/add a function to CakePHP's core file without modifying the original file?
I want a requireNonSecure function because some of my pages have embedded videos that can only be played on our domain name. When using SSL the video hosting service does not recognize our domain name and cannot play the videos.
This is some of the code in the beforeFilter of a controller:
function beforeFilter() {
parent::beforeFilter();
$this->Security->validatePost = false; // disable CSRF protection
$this->Security->blackHoleCallback = 'forceSSL';
$this->Security->requireSecure('pay', 'index');
$this->Auth->allow('index');
}
This is the callback in app_controller.php
function forceSSL() {
$redirect = '';
if (!empty($this->params['url']['redirect'])) {
$redirect = '?redirect=' . $this->params['url']['redirect'];
}
$this->redirect('https://' . rtrim(env('SERVER_NAME'), '/') . $this->here . $redirect);
}
A solution would be to append a function to beforeFilter like this:
In a controller:
function beforeFilter() {
parent::beforeFilter();
// Require non secure (http) for video action
$this->requireNonSecure('video');
// ... other code here
}
In app_controller.php:
function requireNonSecure() {
$requireNonSecure = array_map('strtolower', func_get_args());
if (in_array(strtolower($this->action), $requireNonSecure) || $requireNonSecure == array('*')) {
if ($this->RequestHandler->isSSL()) {
$this->redirect('http://' . rtrim(env('SERVER_NAME'), '/') . $this->here);
return;
}
}
}
This solution adds to SecurityComponent. It should work but there is a risk of redirect loops if both requireSecure and requireNonSecure are set.
SecurityPlusComponent:
class SecurityPlusComponent extends SecurityComponent {
/**
* List of actions that do not require an SSL-secured connection
*
* #var array
* #access public
* #see SecurityPlusComponent::requireNonSecure()
*/
var $requireSecure = array();
/**
* Component startup. All security checking happens here.
*
* #param object $controller Instantiating controller
* #access public
*/
function startup(&$controller) {
$this->_action = strtolower($controller->action);
$this->_methodsRequired($controller);
$this->_secureRequired($controller);
$this->_nonSecureRequired($controller);
$this->_authRequired($controller);
$this->_loginRequired($controller);
$isPost = ($this->RequestHandler->isPost() || $this->RequestHandler->isPut());
$isRequestAction = (
!isset($controller->params['requested']) ||
$controller->params['requested'] != 1
);
if ($isPost && $isRequestAction && $this->validatePost) {
if ($this->_validatePost($controller) === false) {
if (!$this->blackHole($controller, 'auth')) {
return null;
}
}
}
$this->_generateToken($controller);
}
function requireNonSecure() {
$this->_requireMethod('NonSecure', func_get_args());
}
/**
* Check if access requires non secure connection (http)
*
* #param object $controller Instantiating controller
* #return bool true if secure connection required
* #access protected
*/
function _nonSecureRequired(&$controller) {
if (is_array($this->requireNonSecure) && !empty($this->requireNonSecure)) {
$requireNonSecure = array_map('strtolower', $this->requireNonSecure);
if (in_array($this->_action, $requireNonSecure) || $this->requireNonSecure == array('*')) {
if ($this->RequestHandler->isSSL()) {
if (!$this->blackHole($controller, 'nonSecure')) {
return null;
}
}
}
}
return true;
}
}
Modified app_controller forceSSL function:
function securityBlackhole($type) {
$redirect = '';
if (!empty($this->params['url']['redirect'])) {
$redirect = '?redirect=' . $this->params['url']['redirect'];
}
// Force http (non-SSL)
if($type == 'nonSecure') {
$this->redirect('http://' . rtrim(env('SERVER_NAME'), '/') . $this->here . $redirect);
// Force https (SSL)
} else {
$this->redirect('https://' . rtrim(env('SERVER_NAME'), '/') . $this->here . $redirect);
}
}
Would be called like this in a controller:
function beforeFilter() {
parent::beforeFilter();
$this->SecurityPlus->validatePost = false; // disable CSRF protection
$this->SecurityPlus->blackHoleCallback = 'securityBlackhole';
$this->SecurityPlus->requireSecure('pay', 'index');
$this->SecurityPlus->requireNonSecure('video');
$this->Auth->allow('index');
}

Resources