I am using a custom module to restrict a role to a url in drupal 7 the code is as follows:
<?php
// Implements hook_init()
function restrict_access_init() {
$restrictions = restrict_access_restrictions();
global $user;
foreach ($restrictions as $path => $roles) {
// See if the current path matches any of the patterns provided.
if (drupal_match_path($_GET['q'], $path)) {
// It matches, check the current user has any of the required roles
$valid = FALSE;
foreach ($roles as $role) {
print implode("','",$user ->roles);
if (in_array($role, $user->roles)) {
$valid = TRUE;
break;
}
}
if (!$valid) {
drupal_access_denied();
}
}
}
}
function restrict_access_restrictions() {
// This array will be keyed by path and contain an array of allowed roles for that path
return array(
'path/path' => array('admin'),
);
}
?>
This does restrict access just fine but it then renders the page un-styled after the footer.
Any ideas why this may be happening?
I'm at a lost end with this now.
i needed to add module_invoke_all('exit'); exit(); under drupal_acess_denied();
e.g.
drupal_acess_denied();
module_invoke_all('exit');
exit();
Related
I've been working on an application using CakePHP 2.6. We have a class called AuthUser which builds upon the functionality of AuthComponent and allows us to check permissions against our roles for sections in our database.
However I have noticed that our "isAuthorised" function ignores the $this->Auth->allow() which means actions that shouldn't need authorisation are being caught by our checks and this needs to be updated to check properly.
Is it possible to access the $this->Auth->allow() array of actions and if so how would someone go about accessing it?
Below I have included the "isAuthorised" function from the AuthUser class:
public function isAuthorised($controllerName = null) {
//Admin has access to everything
if (AuthUser::isAdmin() === true) {
return true;
}
$roles = array();
//Get the roles allowed for the section
$results = AppController::runStoredProcedure('spGetCurrentSectionRolesForSectionBySectionName', array( $controllerName ));
if (isset($results) && is_array($results)) {
foreach ($results as $row) {
if (isset($row['RoleName'])) {
array_push($roles, $row['RoleName']);
}
}
}
//Check if authenticated user has permission to current controller (is one of the allowed roles)
$userRoles = AuthComponent::user('role');
if (isset($userRoles) && is_array($userRoles)) {
foreach ($userRoles as $key => $value) {
if ($value == true) {5
if (in_array($key, $roles)) {
return true;
}
}
}
}
return false;
}
Please try this
pr($this->Auth->allowedActions);
This will list you all auth->allow() function name that are defined in $this->Auth->allow()
I'm working on a project using CakePHP 3.x.
I have UserAddress, ServiceRequests, Service models.
There is a button on service/view/$id which when clicked will ask user to select address from service-requests/serviceArea which has a list of addresses added by user. service-requests/serviceArea view will contain a select button which when clicked will call add action in ServiceRequests controller with passing two parameters serviceId and userAddressId
This is the serviceArea function created by me.
public function serviceArea($id = null)
{
public $uses = array('UserAddress');
$service = $id;
$query = $userAddresses->find('all')
->where(['UserAddresses.user_id =' => $this->Auth->user('id')]);
$this->set(compact('userAddresses'));
$this->set('_serialize', ['userAddresses']);
}
How to display the address and also pass the $service parameter to the serviceArea view.
I am new to CakePHP, so if you think question is incomplete any edit to it will be appreciated instead of down-voting.
Thank You.
Edit 2
Thank for your answer #jazzcat
After changing my code according to yours and visiting http://domain.com/service-requests/service-area/$id. It is showing error as
Record not found in table "service_requests"
and pointing to the ServiceRequestsController on line no 33
The ServiceRequestController as containing line no 33 is
<?php
namespace App\Controller;
use App\Controller\AppController;
/**
* ServiceRequests Controller
*
* #property \App\Model\Table\ServiceRequestsTable $ServiceRequests
*/
class ServiceRequestsController extends AppController
{
/**
* isAuthorized method
*
*/
public function isAuthorized($user)
{
$action = $this->request->params['action'];
// The add and index actions are always allowed.
if(in_array($action, ['index', 'add', 'serviceRequests'])) {
return true;
}
// All other actions require an id.
if (empty($this->request->params['pass'][0])) {
return false;
}
// Check that the service request belongs to the current user.
$id = $this->request->params['pass'][0];
$serviceRequest = $this->ServiceRequests->get($id); // line : 33
if($serviceRequest->user_id == $user['id']) {
return true;
}
return parent::isAuthorized($user);
}
/* Other actions */
}
?>
This worked for me.
Just added the serviceArea action name in the isAuthorized method
if(in_array($action, ['index', 'add', 'serviceArea'])) {
return true;
}
and it's working fine as expected.
There is alot wrong with your code. Please read the docs
Is the table named user_addresses or user_address ?
You seem to mix the both.
The following would be the correct way to do it assuming your table is named user_addresses
public function serviceArea($id = null)
{
$this->loadModel('UserAddresses');
$userAddresses = $this->UserAddresses->find('all')
->where(['UserAddresses.user_id =' => $this->Auth->user('id')]);
// If you want to filter on the serviceArea ID aswell
if($id)
$userAddresses->andWhere(['id' => $id]);
// Setting SerivceArea ID to compact makes it available in view.
$serviceAreaId = $id;
$this->set(compact('userAddresses', 'serviceAreaId'));
$this->set('_serialize', ['userAddresses']);
}
This snippet:
$id = $this->request->params['pass'][0];
$serviceRequest = $this->ServiceRequests->get($id); // line : 33
Just checks if the first parameter passed to the method exists in ServiceRequests.
(That parameter could be anything, you have to keep that in mind when creating all your methods in that controller, that is to say the least.. bad)
I'm assuming that the service_requests table is associated with the users table and an user_id column exists in the service_requests table.
If that is the case this should work:
public function isAuthorized($user)
{
$action = $this->request->params['action'];
// The add and index actions are always allowed.
if(in_array($action, ['index', 'add'])) {
return true;
}
// Is not authorized if an argument is not passed to the method.
// Don't know why you'd want this , but sure.
if (empty($this->request->params['pass'][0])) {
return false;
}
// Check that the service request belongs to the current user.
$user_id = $this->Auth->user('id');
$serviceRequest = $this->ServiceRequests->find()->where(['ServiceRequests.user_id' => $user_id])->first();
if(!empty($serviceRequest)) {
return true;
}
return parent::isAuthorized($user);
}
let say I have 21 records
my view shows 10 records per page
and now i am on page:3, and then I delete a record and refresh the page
cake will show a not found error
how can I go to page 2(last page), instead of showing a error message ?
I checked the PaginatorComponent.php
it just throw a NotFoundException
if ($requestedPage > $page) {
throw new NotFoundException();
}
according to the Manual you have to catch the exception and redirect to the right page
Here is what I did although I'm not necessarily happy with it.
try {
$this->Paginator->settings = $this->paginate;
$widgets = $this->paginate();
} catch (NotFoundException $e) {
//Redirect to previous page
$query = $this->request->query;
$query['page']--;
extract(Router::parse($this->request->here));
$pass = empty($pass) ? '' : $pass[0];
$this->redirect(array_merge(array('action' => $action, $pass), array('?' => $query)));
}
As you can see I decrement the page number in the query string and keep redirecting that until I reach a valid page. I was unable to find the known number of pages to just direct to the last page. Also, I'm not happy with extracting the parts of the URL just to rebuild it. In my case the $pass param may or may not exist which is why I do the empty check. Although this works, I'd welcome ideas on how to do it better.
What I did was to get the previous page when catching the NoFoundException, but via the named params:
try {
$records = $this->Paginator->paginate();
} catch (NotFoundException $e) {
$this->request->params['named']['page']--;
$records = $this->Paginator->paginate();
}
I think it would be better to just overwrite the paginator component, though.
This works:
```php
class PaginatorComponent extends CorePaginatorComponent
{
/**
* Overwrite to always redirect from out of bounds to last page of paginated collection.
* If pageCount not available, then use first page.
*
* #param \Cake\Datasource\RepositoryInterface|\Cake\Datasource\QueryInterface $object The table or query to paginate.
* #param array $settings The settings/configuration used for pagination.
*
* #throws \Cake\Network\Exception\NotFoundException
*
* #return \Cake\Datasource\ResultSetInterface Query results
*/
public function paginate($object, array $settings = [])
{
try {
$resultSet = parent::paginate($object, $settings);
} catch (NotFoundException $exception) {
$query = null;
if ($object instanceof QueryInterface) {
$query = $object;
$object = $query->repository();
}
$alias = $object->alias();
$lastPage = $this->request->params['paging'][$alias]['pageCount'] > 1 ? $this->request->params['paging'][$alias]['pageCount'] : null;
$response = $this->getController()->redirect(['?' => ['page' => $lastPage] + $this->request->getQuery()]);
// To be please PHPCS and tests, cannot be reached in production.
if (PHP_SAPI === 'cli') {
throw new NotFoundException('Redirect to ' . $response->getHeaderLine('Location') . ' for non-CLI.');
} else {
$response->send();
}
exit();
}
return $resultSet;
}
}
```
Just put that in your project, it will automatically be used instead of the core one.
Works for <=3.4 (First page) and 3.5+ (Last page).
Use a try-catch for pagination and redirect to initial page.
try {
$videos = $this->paginate($videos, $paginate);
} catch (NotFoundException $e) { // Not existing page
return $this->redirect([ // Remove pagination
"controller" => $this->request->getParam('controller'),
"action" => $this->request->getParam('action'),
]);
}
For redirecting to last page, then see answer from #Kris.
I have a CakePHP (latest version) web app with forms and validation all working properly using traditional postback, but now I'm switching some of the forms to submit via ajax. When there are validation errors, I would like to get them back on the client as JSON formatted like so:
{
"success":false,
"errors":{
"data[User][username]":["This is not a valid e-mail address"],
"data[User][password]":["You must choose a password"]
}}
The keys for the errors array need to correspond to the name attribute on the form fields. We have some prebuilt client script that is expecting JSON formatted in this way. The good news is that this is very close to what the validationErrors object looks like in CakePHP. So I'm currently doing this in my controller:
if ($this->User->save($this->request->data)) {
} else {
if ($this->request->is('ajax')) {
$this->autoRender = $this->layout = false;
$response['success'] = false;
$response['errors'] = $this->User->validationErrors;
echo json_encode($response);
exit(0);
}
}
However, this is what the JSON response looks like:
{
"success":false,
"errors":{
"username":["This is not a valid e-mail address"],
"password":["You must choose a password"]
}}
Note that the errors keys have just the basic database table field names in them. They are not converted into data[User][username] format, which the FormHelper usually takes care of.
Is there some easy way to adjust the array before I return it? I don't want to simply loop through and prepend "data[User]" because that is not robust enough. I'd like some code I can put in one place and call from various controllers for various models. What does FormHelper use to come up with the input name attributes? Can I tap into that? Should I somehow use a JSON view?
That's because that's the way the $validationErrors array is formatted. To obtain the output you want you will have to loop through, there's no way around it.
foreach ($this->User->validationErrors as $field => $error) {
$this->User->validationErrors["data[User][$field]"] = $error;
unset($this->User->validationErrors[$field]);
}
I would suggest instead passing all errors to json_encode(). $this->validationErrors is a combined list of all model validation errors for that request available on the view (compiled after render). You should move your display logic (echoing json) into your view, and loop through it there.
in the view
$errors = array();
foreach ($this->validationErrors as $model => $modelErrors) {
foreach ($modelErrors as $field => $error) {
$errors["data[$model][$field]"] = $error;
}
}
$response['errors'] = $errors;
echo json_encode($response);
This would output something like this:
{
"success":false,
"errors": [
"data[User][username]": "This is not a valid e-mail address",
"data[User][password]": "This is not a valid password",
"data[Profile][name]": "Please fill in the field"
]
}
I have created a small recursive function to create validation error as a string with column name so that can be passed as json object.
/**
* prepare erorr message to be displayed from js
*
* #param array $errors validation error array
* #param stringn $message refernce variable
*
* #return void
*/
public function parseValidationErrors($errors, &$message)
{
foreach ($errors as $columnName => $error) {
$message .= "<strong>$columnName:</strong> ";
foreach ($error as $i => $msg) {
if (is_array($msg)) {
$this->_parseValidationErrors($msg, $message);
} else {
$message .= str_replace("This field", "", $msg . " ");
isset($error[$i + 1]) ? $message .= " and " : $message;
}
}
}
}
and controller code goes like this.
if (!$this->YourModel->saveAll($modelObject)) {
$errors = $this->YourModel->validationErrors;
$message = '';
$this->parseValidationErrors($errors, $message);
$response = array('status' => 'error', 'message' => $message);
}
$response['errors']['User'] = $this->User->validationErrors;
When isAuthorized = false the user is redirected to '/' is there a way to change this. I want to redirect to the user dashboard (/users/dashboard) with a flash message saying 'Access prohibited' or something like that.
Cheers!
public function isAuthorized($user) {
if (isset($user['role']) && $user['role'] === 'admin') {
return true; //Admin can access every action
}
return false; // The rest don't
}
If your isAuthorised variable is being evaluated in your controller.
You can call the redirect function.
$this->redirect(array('controller' => 'users', 'action' => 'dashboard'));
If you are actually inside the users controller allready, just call
$this->redirect(array('action' => 'dashboard'));
If not, where are you checking the isAuthorised value?
This is not an ideal solution. However it seems that there is no way to do this with the current built in AuthComponent
Edit: Added code as an example.
public function isAuthorized($user) {
if (parent::isAuthorized($user)) {
return true;
}
// Authorised actions
if (in_array($this->action, array('dashboard'))) {
return true;
}
// Will break out on this call
$this->redirect(array('controller' => 'users', 'action' => 'dashboard'));
return false;
}
I think the best way is to use exception and extends like that :
AppController.php
public function isAuthorized($user) {
throw new ForbiddenException(__('You are not authorized to access.'));
}
AnotherController.php
public function isAuthorized($user) {
if (isset($user['role']) && $user['role'] === 'admin') {
return true;
}
return parent::isAuthorized($user);
}
With this code you are able to manage the roles and the error.
If they are being logged out you can send them where you want with:
$this->Auth->logoutRedirect
I would personally use:
$this->Auth->authError = "You are not authorized to access.";
In order to redirect them to root with a flash message notifying them of the error.
Is a wrong behavior of AuthComponent.
In a nutshell: if the url is visited by a link, the framework is able to reconstruct the path and then redirect to the referring page. Otherwise (by direct entry into a url bar) it fails and it redirects to the home page.
The "bug" is documented and it will be corrected in a future release.
See: http://cakephp.lighthouseapp.com/projects/42648/tickets/591-inconsistent-redirect-behaviour-by-auth-acl
I do a little better than #deep55.
isAuthorized() method can throw an exception no problem, but i think that the inheritance of the Controllers would permit us to improve authorization algorithm using first AppController.isAuthorized(), not last.
So, here is my solution, assuming that I use a user model called Utilisateur, and a role model called Role.
AppController :
/**
* Parent method
*/
public function isAuthorized($user){
App::uses('Utilisateur','Model');
$User = new Utilisateur();
$isAdmin = $User->hasRole(10,$user['id']);
if ($isAdmin) {
return true;
}
}
/**
* Reject unauthorized actions
*/
public function rejectRequest(){
$errorMessage = __("Sorry, you can't do this.");
if ($this->isRest()) {
throw new ForbiddenException($errorMessage);
} else {
$this->Auth->authError = $errorMessage;
$this->Auth->flash['params']['class'] = 'alert-danger';
}
return false ;
}
Utilisateur model :
/**
* hasRole method return true if the user belongs to the correct role group
*/
public function hasRole($role_id, $user_id){
if (!isset($user_id)) {
if (!empty($this->id)) {
$user_id = $this->id ;
} else throw new Exception("Error, parameter $user_id is missing", 1);
}
$user = $this->find('first',array(
'conditions' => array('Utilisateur.id' => $user_id),
'fields' => array('id'),
'contain' => array('Role.id')
));
$roles = $user['Role'];
foreach ($roles as $r) {
if ($role_id == $r['id']) {
return true;
}
}
}
And last, in a specific controller :
/**
* Child method
*/
public function isAuthorized($user){
if (parent::isAuthorized($user)) {
return true;
}
if ( false ) {
return true ;
}
if ( false ) {
return true ;
}
return $this->rejectRequest() ;
}
For Cake version 2, as written in documentation for AuthComponent:
AuthComponent::$unauthorizedRedirect
Controls handling of unauthorized access. By default unauthorized user is redirected to the referrer URL or AuthComponent::$loginRedirect or ‘/’. If set to false a ForbiddenException exception is thrown instead of redirecting.
you can configure AuthComponent to redirect you to custom page in one place using unauthorizedRedirect property.
Just set it in the place where you configure Auth as a component
'Auth' => array(
... other settings...,
'unauthorizedRedirect' => '/users/dashboard'
)
After being redirected, you can print error message defined by authError property
echo $this->Session->flash();
echo $this->Session->flash('auth');
but it will be the same message for any authentication or authorization error.