Jump to content

Page-specific permissions?


doolak

Recommended Posts

I think that ideally you would maintain a separate template for each of those sections, but I understand what you are saying that it may be more hassle to do that. I am interested in updating the CustomPageRoles module to go much further than it currently does, but that's down the road a bit. But I think it would be possible to create a module and hook after Page::editable (like CustomPageRoles is doing with Page::viewable). Your section template should enable access for all the roles that have edit access to any given section. But then your custom editable() function will remove that access when they aren't in the right section. You'll create a new role that has the same name as the section page, and then assign that role to the users that you want to be able to edit that section. Then you'd create an autoload module with logic like this:

public function init() {
$this->addHookAfter('Page::editable', $this, 'editable');
}

public function editable(HookEvent $event) {

$page = $event->object;

// if this isn't a section template, or user is superuser, no need to check further
if($page->template != 'section' || wire('user')->isSuperuser()) return;

// if they had no access at the template, then no need for us to check further
if(!$event->return) return;

// we assume not-editable, until proven otherwise
$editable = false;

// get a role that has the same name as the section page
$role = wire('roles')->get($page->name);

// if the role exists, the user has the role, and the role has edit permission, then it's editable
if($role && wire('user')->hasRole($role) && $role->hasPermission('page-edit') {
$editable = true;
}

$event->return = $editable;
}

Ryan, for some reason I have missed this before. I ended up solving my problem back then by doing 5 separate templates. But now I have similar situation, but this time with 15 sections - so I will definitely take your module as an starting point, thanks!

Link to comment
Share on other sites

Ryan, started to implement this as a full module that switches template based permissions to page based. I have only made few modifications, but it already seems to be working rather nicely! What I couldn't figure out is how to make pageList view to disable pages that aren't viewable for current user?

This is the code I have now:

/**
 * Add the hook
 *
 */
public function init() {
 $this->addHookAfter("Page::viewable", $this, 'viewable');
		    $this->addHookAfter('Page::editable', $this, 'editable');
}
/**
 * Hook called after Page::viewable is called
 *
 */
public function viewable(HookEvent $event) {
 // get the vars we need to check access
 $page = $event->object;
 $user = $this->user;
 // no need to check anything if it's the superuser
 if($user->isSuperuser()) return;
 // don't allow this access control on system templates
 if($page->template->flags & Template::flagSystem) return;
 // get the roles we'll be comparing
 $pageRoles = $page->view_roles;
 $userRoles = $this->user->roles;
 // if page has a 'view_roles' field, but none defined, then inherit 'viewable' state from parent
 if(!count($pageRoles)) {
  // determine access from page's parent, recursively
  $event->return = $page->parent->viewable();
  return;
 }
 // we assume it's not viewable until we can prove otherwise
 $viewable = false;
 // loop through the pageRoles and try to match up to user
 // we don't need to check permissions in the role because
 // all roles are assumed to have page-view permission.
 foreach($pageRoles as $role) {
  if($role->name == 'guest' || $user->hasRole($role)) {
   // guest role was found or the user has the required role
   $viewable = true;
   break;
  }
 }
 $event->return = $viewable;
}

    public function editable(HookEvent $event) {
	    // get the vars we need to check access
	    $page = $event->object;
	    $user = $this->user;
	    // no need to check anything if it's the superuser
 if($user->isSuperuser()) return;
	    // don't allow this access control on system templates
 if($page->template->flags & Template::flagSystem) return;
	    // get the roles we'll be comparing
	    $pageRoles = $page->edit_roles;
	    $userRoles = $this->user->roles;
	    // if page has a 'edit_roles' field, but none defined, then inherit 'editable' state from template or parent
	    if(!count($pageRoles)) {
		    $event->return = $page->parent->editable();
		    return;
	    }
	    // we assume it's not viewable until we can prove otherwise
	    $editable = false;
	    // loop through the pageRoles and try to match up to user
	    // we don't need to check permissions in the role because
	    // all roles are assumed to have page-view / page-edit permission. => actually we should disallow selecting other than roles which have page-edit in first place
	    foreach($pageRoles as $role) {
			    if($role->name == 'guest' || $user->hasRole($role)) {
					    // guest role was found or the user has the required role
					    $editable = true;
					    break;
			    }
	    }
	    // master role for client (would be the only one to edit view_roles and edit_roles fields.
	    if ($user->hasRole("master")) $editable = true;
	    $event->return = $editable;
    }

Above assumes that view_roles and edit_roles are global, so there should never be need to check access from templates. I need to add one more field for "add children" permission. Also a lot of tweaking and testing ahead, but so far it looks very good and doesn't feel "hacky" at all.

Also I am thinking about building a process page which shows all the pages that have defined edit / view roles, so one would not need to browse all the page tree to find the locks...

Link to comment
Share on other sites

Glad to hear you are making progress with this!

ProcessWire handles the template-based permission checking at the DB query level, so something like PageList assumes that anything it gets is okay to include in the output.

Having the permission check at the DB query level is necessary to ensure that things like pagination and 'total' counting are accurate. Though if you aren't often paginating 'pages with permission' vs. 'pages without permission', in the same list, then this may not be a concern.

Since PageList is going to show whatever it gets from $pages->find(), I think your best bet is to hook after $pages->find() and specifically remove the pages the user doesn't have access to:


public function hookPagesFind(HookEvent $event) {
 $items = $event->return;
 foreach($items as $item) {
   if($page doesn't have the role(s) you want) {
     $items->remove($item);
   }
 }
 $event->return = $items;
}

The main thing to note here is that since you are performing this filtering after find(), the quantity of pages returned may be less than what you've specified in your "limit=n" selector.

Link to comment
Share on other sites

Thanks Ryan. I am interested about other possible side-effects of going that route. Do you think it affects performance badly since we are looping all the pages in every request?

I think that the inconsistency with limit is fine, couldn't think scenario where that would be required and ua limited.

I am currently working on this.

Link to comment
Share on other sites

Thanks Ryan. I am interested about other possible side-effects of going that route. Do you think it affects performance badly since we are looping all the pages in every request?

I'm thinking it shouldn't affect performance too much, but haven't tried -- I'm curious what you find here. Since the primary need is just to not show the pages in PageList, you could set it to do this only when ProcessPageList is active: if(wire('page')->process == 'ProcessPageList') { }

This is taking shape nicely - Ryan, do you mind adding buildFormRoles method hookable in ProcessPageEdit.module? I would like to hide it all together and use own implementation for same information.

No problem. I've just made it hookable and will push to GitHub within the next hour (along with some other minor updates).

Link to comment
Share on other sites

I think that doing a hook replacement of buildFormRoles should remove that entirely. If you hook in before and set it to replace, then it'll never even call PageEdit's buildFormRoles method. Set $event->return to return a blank InputfieldWrapper, InputfieldHidden or InpufieldMarkup.

public function hookBeforeBuildFormRoles($event) {
 $event->replace = true; 
 $event->return = new InputfieldWrapper();
}

(on mobile so can't test at the moment, but think that should work)

Link to comment
Share on other sites

I think that doing a hook replacement of buildFormRoles should remove that entirely. If you hook in before and set it to replace, then it'll never even call PageEdit's buildFormRoles method. Set $event->return to return a blank InputfieldWrapper, InputfieldHidden or InpufieldMarkup.

public function hookBeforeBuildFormRoles($event) {
$event->replace = true;
$event->return = new InputfieldWrapper();
}

(on mobile so can't test at the moment, but think that should work)

This works, thanks!

Link to comment
Share on other sites

  • 3 weeks later...

I have been developing this module little further. There are some nasty problems where I could need some help. This is my current code:

<?php
class CustomPageRoles extends WireData implements Module {
/**
 * Provide information about this module to ProcessWire
 *
 */
public static function getModuleInfo() {
 return array(
  'title' => 'Custom Page Roles',
  'summary' => 'Control viewable and editable roles at the page level.',
  'version' => 002,
  'permanent' => false,
  'autoload' => true,
  'singular' => true,
  );
}
/**
 * Add the hook
 *
 */
public function init() {
 $this->addHookAfter("Page::viewable", $this, 'viewable');
 $this->addHookAfter("Page::editable", $this, 'editable');
 $this->addHookAfter("Page::addable", $this, 'addable');
 $this->addHookAfter("Page::deleteable", $this, 'deleteable');
 $this->addHookAfter("Page::sortable", $this, 'sortable');
 //$this->addHookAfter("Pages::find", $this, 'hookPagesFind');
 $this->addHookAfter("InputfieldCheckboxes::render", $this, 'hideRoleCheckboxes');

 //$this->addHookBefore("ProcessPageEdit::buildFormRoles", $this, 'hideFormRoles');
}

public function hideFormRoles(HookEvent $event) {

 $event->replace = true;
 $event->return = new InputfieldWrapper();
}

/**
 * This toggles the edit_roles and view_roles for a text list for regular users (not superuser or client's master role)
 *
 */
public function hideRoleCheckboxes(HookEvent $event) {
 $user = $this->user;
 $page = $this->page;


 if ($user->hasRole("paakayttaja") || $user->isSuperuser()) return;

 // This is pretty ugly hack to just hide visually those UA fields
 if ($event->object->name == "edit_roles" || $event->object->name == "view_roles") {
  $event->return = "<div style='display:none;'>{$event->return}</div>";
 }

}

public function hookPagesFind(HookEvent $event) {
 $items = $event->return;
 foreach($items as $item) {
  if($item->template->flags & Template::flagSystem) {
   // don't allow this access control on system templates
  } else if (!$item->viewable()) {
   $items->remove($item);
  }
 }
 $event->return = $items;
}
/**
 * Hook called after Page::viewable is called
 *
 */
public function viewable(HookEvent $event) {
 // get the vars we need to check access
 $page = $event->object;
 $user = $this->user;
 // no need to check anything if it's the superuser
 if($user->isSuperuser()) return;
 // don't allow this access control on system templates
 if($page->template->flags & Template::flagSystem) return;

 if ($page->is(Page::statusUnpublished)) return;
 // get the roles we'll be comparing
 $pageRoles = $page->view_roles;
 $userRoles = $this->user->roles;

 // If there ain't no view_roles, then fallback to template permissions
 if (!$pageRoles) return;
 // if page has a 'view_roles' field, but none defined, then inherit 'viewable' state from parent
 if(!count($pageRoles)) {
  // determine access from page's parent, recursively
  // We check if there is at least one parent with edit_roles
  foreach($page->parents() as $p) {
   if (count($p->view_roles)) {
 $event->return = $p->viewable();
 return;
   }
  }

  // If not view_roles found from parents, then fallback to template access
  return;
 }
 // we assume it's not viewable until we can prove otherwise
 $viewable = false;
 // loop through the pageRoles and try to match up to user
 // we don't need to check permissions in the role because
 // all roles are assumed to have page-view permission.
 foreach($pageRoles as $role) {
  if($role->name == 'guest' || $user->hasRole($role)) {
   // guest role was found or the user has the required role
   $viewable = true;
  }
 }
 $event->return = $viewable;
}

public function editable(HookEvent $event) {

 // get the vars we need to check access
 $page = $event->object;
 $user = $this->user;

 // no need to check anything if it's the superuser
 if($user->isSuperuser()) return;
 // don't allow page based access control on system templates
 if($page->template->flags & Template::flagSystem) return;

 // get the roles we'll be comparing
 $pageRoles = $page->edit_roles;
 $userRoles = $this->user->roles;

 // if page has a 'edit_roles' field, but none defined, then inherit 'editable' state from parent(s)
 if(!count($pageRoles)) {
  // We check if there is at least one parent with edit_roles
  foreach($page->parents() as $p) {
   if (count($p->edit_roles)) {
 $event->return = $p->editable();
 return;
   }
  }

  // If not edit_roles found from parents, then fallback to template access
  return;
 }

 // we assume it's not editable until we can prove otherwise
 $editable = false;

 // loop through the pageRoles and try to match up to user
 // we don't need to check permissions in the role because
 // all roles are assumed to have page-view / page-edit permission. => actually we should disallow selecting other than roles which have page-edit in first place
 foreach($pageRoles as $role) {
  if ($user->hasRole($role)) {
   $editable = true;
   break;
  }
 }

 // This would be a setting, a "master role" for client
 if ($user->hasRole("paakayttaja")) $editable = true;

 $event->return = $editable;
}

/**
 * Add children permission is granted together with edit, at least for now
 *
 */
public function addable(HookEvent $event) {
 $page = $event->object;

 // If page is editable and generally can have children, then allow addable
 if ($page->editable() && !$page->template->noChildren) {
  $event->return = true;
 } else {
  $event->return = false; 
 }
}

public function deleteable(HookEvent $event) {
 $page = $event->object;

 if ($page->editable()) {
  $event->return = true;
 } else {
  $event->return = false;
 }
}

public function sortable(HookEvent $event) {
 $page = $event->object;

 if ($page->editable()) {
  $event->return = true;
 } else {
  $event->return = false;
 }
}

/**
 * Install the module
 *
 */
public function ___install() {
 if($this->fields->get('view_roles')) {
  $this->error("You already have a 'view_roles' field.");
  return;
 }

    if($this->fields->get('edit_roles')) {
  $this->error("You already have a 'edit_roles' field.");
  return;
 }

 $field = new Field();
 $field->type = $this->modules->get("FieldtypePage");
 $field->name = 'view_roles';
 $field->label = 'Roles that can view this page';
 $field->description =
  "Check the roles that may view this page. At least one role must be checked (like one of your roles or superuser), " .
  "otherwise this page does not define access. When access is not defined here, it is inherited from the template " .
  "or parent(s). If this page's template allows 'view' access to the user, then it will check the parent(s). " .
  "Access will inherit from parents that also have a custom page roles field. ";
 $field->derefAsPage = 0;
 $field->parent_id = $this->config->rolesPageID;
 $field->labelFieldName = 'name';
 $field->inputfield = 'InputfieldCheckboxes';
 $field->save();

    $this->message("Added fields 'view_roles'. Add this field to any templates where you want to control view access.");

    $field = new Field();
 $field->type = $this->modules->get("FieldtypePage");
 $field->name = 'edit_roles';
 $field->label = 'Roles that can edit this page';
 $field->description =
  "Check the roles that may edit this page. At least one role must be checked (like one of your roles or superuser), " .
  "otherwise this page does not define access. When access is not defined here, it is inherited from the template " .
  "or parent(s). If this page's template allows 'view' access to the user, then it will check the parent(s). " .
  "Access will inherit from parents that also have a custom page roles field. ";
 $field->derefAsPage = 0;
 $field->parent_id = $this->config->rolesPageID;
 $field->labelFieldName = 'name';
 $field->inputfield = 'InputfieldCheckboxes';
 $field->save();
 $this->message("Added fields 'edit_roles'. Add this field to any templates where you want to control edit access.");
}
/**
 * Uninstall the module
 *
 */
public function ___uninstall() {
 $this->message("To complete uninstall, remove the 'view_roles' and 'edit_roles' field from all templates and then delete the fields.");
}
}

It has one hard coded attribute (role name "paakayttaja" ["superuser for client"], which will of course be a setting later on). This code works fine, you can choose which templates can have page based user access. This allows to choose edit and view roles (superuser and "paakayttaja") can make the selections. But when using this module users who get their edit access through page permissions cannot create new pages, but get this error message:

You don't have access to the template required to add pages here

This can be avoided by adding hook into ProcessPageAdd::isAllowedTemplate and putting $event->return = true; there. This is my first problem:

1) How should I handle this situation? I do want to allow people to create a new pages, but not just any pages - they cannot choose any template (ie add "home" page somewhere). So we definitely need template restrictions. One way to think of it is that we always need to have restricted bunch of templates allowed in family tab. Even if I have only one template allowed, I get the error. Would it be good solution to look for allowed templates for children, and then return true for those templates? That would also require the ProcesssPageAdd::isAllowedTemplate hook. Not really sure what is best way to go here - any ideas?

2) This is much simpler one. I want to have those view_roles and edit_roles be editable only for "paakayttaja" role. How can this be achieved now? Currently I have ugly hack to just hide the selections (see hideRoleCheckboxes method). This also falls apart if one changes the inputfield for edit_roles or view_roles field.

Other than that: when we get this working I think we have pretty much perfect UA in my opinion. Simple and powerful template based UA + page based permissions only there where they are needed (ie. when you create site for football team that has 15 junior teams and they all need similar area on the site).

Link to comment
Share on other sites

How should I handle this situation? I do want to allow people to create a new pages, but not just any pages - they cannot choose any template (ie add "home" page somewhere). So we definitely need template restrictions.

Perhaps having permissions at the template level should be a pre-requisite to making them definable at the page level? Meaning, if you didn't have this module installed, then the permissions would need to be setup such that the user would be able to create the pages. I thought this was the case before, but might not be remembering it correctly.

Would it be good solution to look for allowed templates for children, and then return true for those templates?

This seems like a good way to go. It should also probably return false when the template defines access that doesn't include the user's role. So if you use the strategy above, this hook would be more about selectively denying access than allowing it.

2) This is much simpler one. I want to have those view_roles and edit_roles be editable only for "paakayttaja" role. How can this be achieved now? Currently I have ugly hack to just hide the selections (see hideRoleCheckboxes method). This also falls apart if one changes the inputfield for edit_roles or view_roles field.

Maybe best to remove these fields entirely from the form at runtime using the ProcessPageEdit::buildForm hook:

public function hookBuildForm(HookEvent $event) {
 if(wire('user')->hasRole('paakayttaja')) return;
 $form = $event->return;
 $field = $form->get('view_roles');
 if($field) $field->parent->remove($field);
 $field = $form->get('edit_roles');
 if($field) $field->parent->remove($field);
 $event->return = $form;
}
Link to comment
Share on other sites

Thanks Ryan.

Perhaps having permissions at the template level should be a pre-requisite to making them definable at the page level? Meaning, if you didn't have this module installed, then the permissions would need to be setup such that the user would be able to create the pages. I thought this was the case before, but might not be remembering it correctly.

This might work, but I need to think about it little bit more. Do you mean that if I have template, say "team-front" and then 20 roles like "boys-02", "boys-03" etc.. I would then edit team-front template and let all those roles to edit and create pages with that template. After that I would be able to select those 20 roles from page-edit view? It wouldn't be as "ad-hoc" as i would like to be, but also gives even more granular control. I think I look into this approach next!

Maybe best to remove these fields entirely from the form at runtime using the ProcessPageEdit::buildForm hook:

I think that is what I did first. At least when checkboxes are used it cleared all selections when those checkboxes were missing. That is why I ended with that simple hiding trick.

Link to comment
Share on other sites

Do you mean that if I have template, say "team-front" and then 20 roles like "boys-02", "boys-03" etc.. I would then edit team-front template and let all those roles to edit and create pages with that template.

Only if the template is one that defines access. Templates that already inherit access would still inherit. I guess that the most common use case would be that you'd set all your templates to not define access except for 'home' and 'admin'. You'd give all those editor roles edit access on the home template, which would then be inherited by all your others. Then your page-level access control would come in to place limits on that. But I think this would solve the issue with the "you aren't allowed" error you were getting. But I also think it would be a good way to go in general, as its building upon the access control already there (which saves some work) rather than just replacing it. In your viewable() or editable() hooks, if they are coming back 'false' then you don't even need to perform page-level access checking.

I think that is what I did first. At least when checkboxes are used it cleared all selections when those checkboxes were missing. That is why I ended with that simple hiding trick.

Ah, I know what you mean. This is one thing I don't like about checkboxes... no way to tell the difference between unchecked and not-present. I was thinking removing them would still work, since they would not be present in the form for the render or the process, but maybe I'm overlooking something. You might try this (below) instead, which doesn't remove them but designates them as hidden and thus not rendered or processed:

public function hookBuildForm(HookEvent $event) {
 if(wire('user')->hasRole('paakayttaja')) return;
 $form = $event->return;
 $field = $form->get('view_roles');
 if($field) $field->collapsed = Inputfield::collapsedHidden;
 $field = $form->get('edit_roles');
 if($field) $field->collapsed = Inputfield::collapsedHidden;
 $event->return = $form;
}

If that doesn't do it, we might try and just get away from checkboxes and use either radio buttons, select multiple or asmSelect instead (something that have a distinction between not present and not checked).

Link to comment
Share on other sites

Ryan, that collapsedHidden did the job, thanks!

About setting template permissions first and then filtering those per page basis.. it works perfectly with view permission, where we want lot's of roles first see, end then restrict on deeper levels. But edit permissions usually go other way: home page has less editors and then as we go deeper sections then we add more editors.

In your viewable() or editable() hooks, if they are coming back 'false' then you don't even need to perform page-level access checking.

Not sure I understand. If I have restricted access on page (template: "team-front") level to be editable only for "boys-03" and it's children (and their children and so on) then don't define access. I do need to loop through parents to find that restrictions - if I just fall back to template access, then all roles that can edit team-front can edit also those children. But if I do keep my parents loop then it should work. While I think it wouldn't usually work from home template (I cannot allow all with edit access there), I think it would work just fine on "team-front" template and others like that. So I will definitely try that out next. I also like that when build on that way, then editors doesn't see all the possible roles when choosing who can edit, but only the ones that site builder has defined could be editors there.

Link to comment
Share on other sites

This seems to be working very well. Simplified the code a lot! Here is the current version:


<?php

class CustomPageRoles extends WireData implements Module {

/**
* Provide information about this module to ProcessWire
*
*/
public static function getModuleInfo() {
return array(
'title' => 'Custom Page Roles', 
'summary' => 'Control viewable and editable roles at the page level.', 
'version' => 002, 
'permanent' => false, 
'autoload' => true, 
'singular' => true, 
);
}

/**
* Add the hook
*
*/
public function init() {
$this->addHookAfter("Page::viewable", $this, 'viewable');
$this->addHookAfter("Page::editable", $this, 'editable');
$this->addHookAfter("Page::addable", $this, 'addable');
//$this->addHookAfter("Pages::find", $this, 'hookPagesFind');
$this->addHookAfter("ProcessPageEdit::buildForm", $this, 'hookBuildForm');
}

public function hookBuildForm(HookEvent $event) {
if(wire('user')->hasRole('paakayttaja')) return;
if(wire('user')->hasRole('superuser')) return;
$form = $event->return;
$field = $form->get('view_roles');
if($field) $field->collapsed = Inputfield::collapsedHidden;
$field = $form->get('edit_roles');
if($field) $field->collapsed = Inputfield::collapsedHidden;
$event->return = $form;
}

public function hookPagesFind(HookEvent $event) {
$items = $event->return;
foreach($items as $item) {
if($item->template->flags & Template::flagSystem) {
// don't allow this access control on system templates
} else if (!$item->viewable()) {
$items->remove($item);
}
}
$event->return = $items;
}

/**
* Hook called after Page::viewable is called
*
*/
public function viewable(HookEvent $event) {

// get the vars we need to check access
$page = $event->object; 
$user = $this->user; 

// no need to check anything if it's the superuser
if($user->isSuperuser()) return;

// don't allow this access control on system templates
if($page->template->flags & Template::flagSystem) return;

if ($page->is(Page::statusUnpublished)) return;

// get the roles we'll be comparing
$pageRoles = $page->view_roles; 
$userRoles = $this->user->roles;

// If there ain't no view_roles, then fallback to template permissions
if (!$pageRoles) return;

// if page has a 'view_roles' field, but none defined, then inherit 'viewable' state from parent
if(!count($pageRoles)) {
// determine access from page's parent, recursively 
// We check if there is at least one parent with edit_roles
foreach($page->parents() as $p) {
if (count($p->view_roles)) {
$event->return = $p->viewable();
return;
}
}

// If not view_roles found from parents, then fallback to template access
return; 
}

// we assume it's not viewable until we can prove otherwise
$viewable = false; 

// loop through the pageRoles and try to match up to user
// we don't need to check permissions in the role because
// all roles are assumed to have page-view permission. 
foreach($pageRoles as $role) {
if($role->name == 'guest' || $user->hasRole($role)) {
// guest role was found or the user has the required role
$viewable = true; 
}
} 
$event->return = $viewable;
}

public function editable(HookEvent $event) {

// get the vars we need to check access
$page = $event->object; 
$user = $this->user;

// no need to check anything if it's the superuser
if($user->isSuperuser()) return;

// don't allow page based access control on system templates
if($page->template->flags & Template::flagSystem) return; 

// get the roles we'll be comparing
$pageRoles = $page->edit_roles; 
$userRoles = $this->user->roles;


// if page has a 'edit_roles' field, but none defined, then inherit 'editable' state from parent(s)
if(!count($pageRoles)) {
// We check if there is at least one parent with edit_roles
foreach($page->parents() as $p) {
if (count($p->edit_roles)) {
$event->return = $p->editable();
return;
}
}

// If not edit_roles found from parents, then fallback to template access
return; 
}

// we assume it's not editable until we can prove otherwise
$editable = false; 

// loop through the pageRoles and try to match up to user
// we don't need to check permissions in the role because
// all roles are assumed to have page-view / page-edit permission. => actually we should disallow selecting other than roles which have page-edit in first place
foreach($pageRoles as $role) {
if ($user->hasRole($role)) {
$editable = true; 
break;
}
}

// This would be a setting, a "master role" for client
if ($user->hasRole("paakayttaja")) $editable = true;

$event->return = $editable;
}

/**
* Add children permission is granted together with edit, at least for now
*
*/
public function addable(HookEvent $event) {
$page = $event->object;

// If page is editable and generally can have children, then allow addable
if ($page->editable() && !$page->template->noChildren) {
$event->return = true; 
} else {
$event->return = false; 
}
}

/**
* Install the module
*
*/
public function ___install() {

if($this->fields->get('view_roles')) {
$this->error("You already have a 'view_roles' field."); 
return;
}

       if($this->fields->get('edit_roles')) {
$this->error("You already have a 'edit_roles' field."); 
return;
}

$field = new Field();
$field->type = $this->modules->get("FieldtypePage"); 
$field->name = 'view_roles';
$field->label = 'Roles that can view this page'; 
$field->description = 
"Check the roles that may view this page. At least one role must be checked (like one of your roles or superuser), " . 
"otherwise this page does not define access. When access is not defined here, it is inherited from the template " . 
"or parent(s). If this page's template allows 'view' access to the user, then it will check the parent(s). " . 
"Access will inherit from parents that also have a custom page roles field. ";
$field->derefAsPage = 0;
$field->parent_id = $this->config->rolesPageID; 
$field->labelFieldName = 'name';
$field->inputfield = 'InputfieldCheckboxes';
$field->save();

       $this->message("Added fields 'view_roles'. Add this field to any templates where you want to control view access."); 

       $field = new Field();
$field->type = $this->modules->get("FieldtypePage"); 
$field->name = 'edit_roles';
$field->label = 'Roles that can edit this page'; 
$field->description = 
"Check the roles that may edit this page. At least one role must be checked (like one of your roles or superuser), " . 
"otherwise this page does not define access. When access is not defined here, it is inherited from the template " . 
"or parent(s). If this page's template allows 'view' access to the user, then it will check the parent(s). " . 
"Access will inherit from parents that also have a custom page roles field. ";
$field->derefAsPage = 0;
$field->parent_id = $this->config->rolesPageID; 
$field->labelFieldName = 'name';
$field->inputfield = 'InputfieldCheckboxes';
$field->findPagesCode = '
$editRoles = $page->getAccessTemplate()->editRoles;
$editRolePages = new PageArray;
foreach($editRoles as $roleId) {
$editRolePages->add($pages->get($roleId));
}
return $editRolePages;
';
$field->save();

$this->message("Added fields 'edit_roles'. Add this field to any templates where you want to control edit access."); 
}

/**
* Uninstall the module
*
*/
public function ___uninstall() {
$this->message("To complete uninstall, remove the 'view_roles' and 'edit_roles' field from all templates and then delete the fields."); 
}
}

I had to keep the addable hook, otherwise it would show the "add" button on pagelist on places where user isn't actually allowed to add pages (allowed by template, but restricted be page). This same problem is with "clone" and not sure how I can get rid of it, since that comes from additional module by hooking...? Other than that my quick tests have worked perfectly.

Any ideas of optimizations or further ideas? I will soon push this to github and modules directory. Also I need to write good docs for this.

  • Like 3
Link to comment
Share on other sites

No comments on the code itself at the moment (looking good though!), but the texts could be translatable right from the beginning don't you think? And of course that hard coded 'paakayttaja' into a setting, but that one you already mentioned earlier.

I think we'll have to take a detailed look into this someday soon at work, will be used here as well for sure. Nice module!

Link to comment
Share on other sites

I shared this already a couple times, but thought copy it here also.

I have a site where there's language folders on the root level. To give different users only access to edit and add pages in a branch with all children inherited, or just a single page. I use a page field on user template to sepcify which page. Then use this module code.

..
public function init() {
  if($this->user->hasRole("editor")) $this->addHookAfter("Page::editable", $this, 'checkpermission'); 
  if($this->user->hasRole("editor")) $this->addHookAfter("Page::addable", $this, 'checkpermission');
  //if($this->user->hasRole("client")) $this->addHookAfter("Page::viewable", $this, 'checkpermission'); 
}

public function checkpermission(HookEvent $event) {

  // if it was already determined they don't have access, then abort
  if(!$event->return) return;

  $page = $event->object; 
  $parents = $page->parents;

// run check for parent pages and allow recursive inherit access 
foreach($parents as $p){
 if($this->user->editable_pages->has($p)) return true;
}
// then run check on selected single pages
  if(!$this->user->editable_pages->has($page)) $event->return = false; 
}
...
Link to comment
Share on other sites

No comments on the code itself at the moment (looking good though!), but the texts could be translatable right from the beginning don't you think? And of course that hard coded 'paakayttaja' into a setting, but that one you already mentioned earlier.

I thought about text translations, but not sure how to implement these. Since only strings that should be translatable are those edit_roles and view_roles field labels and descriptions. And since they are used in install method, they will go right into default language values. So for me it is not clear how to make this translatable? Or then I am just missing the obvious...?

About hard coded role... not sure why I have used role there, it should be permission of course (so no need for settings). i'll make that fix quickly.

Link to comment
Share on other sites

Any ideas of optimizations or further ideas?

I'm having difficulty following the code due to IP.Board removing all indentation. If you can post the .module file or add it to the modules directory, I can get a better look. :)

I thought about text translations, but not sure how to implement these. Since only strings that should be translatable are those edit_roles and view_roles field labels and descriptions. And since they are used in install method, they will go right into default language values. So for me it is not clear how to make this translatable? Or then I am just missing the obvious...?

No need to make them translatable in the module itself. They should already be translatable from the fields editor. It looks to me like you've got this setup correctly.

Link to comment
Share on other sites

I'm having difficulty following the code due to IP.Board removing all indentation. If you can post the .module file or add it to the modules directory, I can get a better look. :)

https://github.com/apeisa/CustomPageRoles/blob/master/CustomPageRoles.module It will soon be on the modules directory also, but want to write some readme and test it a little more.

No need to make them translatable in the module itself. They should already be translatable from the fields editor. It looks to me like you've got this setup correctly.

Yeah, that is true. What would be supernice is that the module itself would have ready translations for few languages (well, at least for Finnish). But to make that rock I would need to know how each language is called. And since we don't require strict naming conventions (which is good, we are already using different customers as languages on our help site) I don't have any way to know. This is very minor thing, but it is boring to do translation each time this module is installed.

  • Like 2
Link to comment
Share on other sites

  • 6 months later...
  • 3 weeks later...

Hi Ryan,

Is your module working with 2.3?

Everything works admin side (field is added, and I can select roles), but I can still view the page on the frontend.

I've tried the obvious (making sure I didn't have the role, logging out, etc...) — no luck.

False alarm. The page wasn't actually viewable. I got confused because it is still showing in my nav (using MarkupSimpleNavigation).

  • Like 1
Link to comment
Share on other sites

  • 1 month later...

Hi,

I tested the module "Custom Page Roles" but it´s not what I thougt.

I want to give a user an edit role to specific pages so, that he only has listened in PW-Admin the pages he has access to edit.

Is it possible to do that?

Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...