editable()) { do something } * if(!$page->viewable()) { echo "sorry you can't view this"; } * ...and so on... * * ProcessWire 2.x * Copyright (C) 2014 by Ryan Cramer * Licensed under GNU/GPL v2, see LICENSE.TXT * * http://processwire.com * * Optional special permissions that affect $page->editable() behavior: * * 1. page-publish: when installed, editable() returns false, when it would otherwise return true, * on published pages, if user doesn't have page-publish permission in their roles. * * 2. page-edit-created: when installed, editable() returns false, when it would otherwise * return true, if user's role has this permission and they are not the $page->createdUser. * This is a permission that reduces access rather than increasing it. Note that * page-edit-created does nothing if the user doesn't have page-edit permission. * */ class PagePermissions extends WireData implements Module { public static function getModuleInfo() { return array( 'title' => 'Page Permissions', 'version' => 105, 'summary' => 'Adds various permission methods to Page objects that are used by Process modules.', 'permanent' => true, 'singular' => true, 'autoload' => true, ); } /** * Do we have page-publish permission in the system? * * @var null|bool Null=unset state * */ protected $hasPagePublish = null; /** * Do we have page-edit-created permission in the system? * * @var null|bool Null=unset state * */ protected $hasPageEditCreated = null; /** * Establish permission hooks * */ public function init() { $this->addHook('Page::editable', $this, 'editable'); $this->addHook('Page::publishable', $this, 'publishable'); $this->addHook('Page::viewable', $this, 'viewable'); $this->addHook('Page::listable', $this, 'listable'); $this->addHook('Page::deleteable', $this, 'deleteable'); $this->addHook('Page::deletable', $this, 'deleteable'); $this->addHook('Page::trashable', $this, 'trashable'); $this->addHook('Page::addable', $this, 'addable'); $this->addHook('Page::moveable', $this, 'moveable'); $this->addHook('Page::sortable', $this, 'sortable'); // $this->addHook('Template::createable', $this, 'createable'); } /** * Is the page editable by the current user? * * A field name may optionally be specified as the first argument, in which case the field on that page will also be checked for access. * */ public function editable($event) { $page = $event->object; if(!$this->pageEditable($page)) { $event->return = false; } else if(isset($event->arguments[0])) { $event->return = $this->fieldEditable($page, $event->arguments[0]); } else { $event->return = true; } } /** * Is the given page editable? * */ protected function pageEditable(Page $page) { $user = $this->wire('user'); // superuser can always do whatever they want if($user->isSuperuser()) return true; // note there is an exception in the case of system pages, which require superuser to edit if($page->status & Page::statusSystem) return false; // If page is locked and user doesn't have permission to unlock, don't let them edit if($page->status & Page::statusLocked) { if(!$user->hasPermission("page-lock")) return false; } // special conditions apply if the page is a User if($page instanceof User || $page->template->id == $this->wire('config')->userTemplateID) { // if the current process is something other than ProcessUser, they don't have permission if($this->wire('process') != 'ProcessUser') return false; // if the user page being edited has a superuser role, and the current user doesn't, never let them edit regardless of any other permissions $suRole = $this->wire('roles')->get($this->wire('config')->superUserRolePageID); if($page->roles->has($suRole) && !$user->roles->has($suRole)) return false; // if we reach this point and user has user-admin permission, then they are good if($user->hasPermission('user-admin')) return true; } // if the user doesn't have page-edit permission, don't let them go further if(!$user->hasPermission("page-edit", $page)) return false; // check if the system has a page-edit-created permission installed if(is_null($this->hasPageEditCreated)) $this->hasPageEditCreated = $this->wire('permissions')->get('page-edit-created')->id > 0; if($this->hasPageEditCreated) { // page-edit-created permission is installed, so we have to account for it // if user is not the one that created this page, don't allow them to edit it if($page->created_users_id != $user->id) return false; } // now check if there is a page-publish permission in the system, and use it if so if(is_null($this->hasPagePublish)) $this->hasPagePublish = $this->wire('permissions')->get('page-publish')->id > 0; if($this->hasPagePublish) { // if user has the page-publish permission here, then we're good if($user->hasPermission('page-publish', $page)) return true; // if the page is unpublished then we're fine too if($page->is(Page::statusUnpublished)) return true; // otherwise user cannot edit this page return false; } return true; } /** * Assuming the page is editable, is the given field name also editable? * */ protected function fieldEditable(Page $page, $name) { if(!is_string($name)) return false; if(!strlen($name)) return true; if($name == 'id' && ($page->status & Page::statusSystemID)) return false; $user = $this->wire('user'); if($page->status & Page::statusSystem) { if(in_array($name, array('id', 'name', 'template', 'templates_id', 'parent', 'parent_id'))) { return false; } } if($name == 'template' || $name == 'templates_id') { if($page->template->noChangeTemplate) return false; if(!$user->hasPermission('page-template', $page)) return false; } if($name == 'parent' || $name == 'parent_id') { if($page->template->noMove) return false; if(!$user->hasPermission('page-move', $page)) return false; } if($name == 'sortfield') { if(!$user->hasPermission('page-sort', $page)) return false; } if($name == 'roles') { if(!$user->hasPermission('user-admin')) return false; } // FUTURE: check per-field edit access return true; } /** * Is the page viewable by the current user? * * @param HookEvent $event * */ public function viewable($event) { $page = $event->object; $viewable = true; $user = $this->wire('user'); $u = $event->arguments(0); // allow specifying User instance as argument 0 // this gives you a "viewable to user" capability if($u && $u instanceof User) $user = $u; if($page->status >= Page::statusUnpublished) $viewable = false; else if(!$page->template || !$page->template->filenameExists()) $viewable = false; else if($user->isSuperuser()) $viewable = true; else if($page->process) $viewable = $this->processViewable($page->process); else if(!$user->hasPermission("page-view", $page)) $viewable = false; else if($page->isTrash()) $viewable = false; // if the page is editable by the current user, force it to be viewable if(!$viewable && !$user->isGuest() && $page->is(Page::statusUnpublished)) { if($page->editable() && $page->template->filenameExists()) $viewable = true; } $event->return = $viewable; } /** * Does the user have explicit permission to access the given process? * * Access to the process takes over 'page-view' access to the page so that the administrator * doesn't need to setup a separate role just for 'view' access in the admin. Instead, they just * give the existing roles access to the admin process and then 'view' access is assumed for that page. * */ protected function processViewable($process) { $user = $this->wire('user'); if($user->isGuest()) return false; if($user->isSuperuser()) return true; $info = $this->wire('modules')->getModuleInfo($process); $permissionName = empty($info['permission']) ? '' : $info['permission']; // if the process module doesn't explicitely define a permission, // then we assume the user doesn't have access if(!$permissionName) return false; return $user->hasPermission($permissionName); } /** * Is the page listable by the current user? * * A listable page may appear in a listing, but doesn't mean that the user can actually * view the page or that the page is renderable. * */ public function listable($event) { $page = $event->object; $user = $this->wire('user'); $listable = true; if($user->isSuperuser()) $listable = true; else if($page instanceof User && $user->hasPermission('user-admin')) $listable = true; else if($page->is(Page::statusUnpublished) && !$page->editable()) $listable = false; else if($page->process && !$this->processViewable($page->process)) $listable = false; else if($page->isTrash()) $listable = false; else if($page->getAccessTemplate()->guestSearchable) $listable = true; else if(!$user->hasPermission("page-view", $page)) $listable = false; $event->return = $listable; } /** * Is the page deleteable by the current user? * */ public function deleteable($event) { $page = $event->object; $user = $this->wire('user'); if($page instanceof User && $user->hasPermission('user-admin')) { $deleteable = true; if($page->id == $user->id) $deleteable = false; // can't delete self if($page->hasRole('superuser') && !$user->hasRole('superuser')) $deleteable = false; // non-superuser can't delete superuser } else { $deleteable = $this->pages->isDeleteable($page); if($deleteable && !$user->isSuperuser()) { // make sure the page is editable and user has page-delete permission, if not dealing with superuser $deleteable = $page->editable() && $user->hasPermission("page-delete", $page); } } $event->return = $deleteable; } /** * Is the page trashable by the current user? * */ public function trashable($event) { $page = $event->object; if($page->is(Page::statusTrash) || $page->template->noTrash) { // if page is already in trash, or template doesn't allow placement in trash, we return false $event->return = false; return; } $this->deleteable($event); } /** * Can the current user add child pages to this page? * * Optionally specify the page to be added as the first argument for additional access checking. * i.e. if($page->addable($somePage)) * */ public function addable($event) { $page = $event->object; $user = $this->wire('user'); $addable = false; if($page->template->noChildren) { $addable = false; } else if($user->isSuperuser()) { $addable = true; } else if($page->id == $this->wire('config')->usersPageID && $user->hasPermission('user-admin')) { // users with user-admin access adding a page to users: add access is assumed // rather than us having a separate 'users' template where access is defined $addable = true; } else if($user->hasPermission('page-add', $page)) { // user has page-add permission, now we need to check that they have access // on the templates in this context $addable = $this->addableTemplate($page, $user); } // check if a $page is provided as the first argument for additional access checking if($addable && isset($event->arguments[0]) && $event->arguments[0] instanceof Page) { $addPage = $event->arguments[0]; if(count($page->template->childTemplates) && !in_array($addPage->template->id, $page->template->childTemplates)) { $addable = false; } } $event->return = $addable; } /** * Checks that a parent is addable within the context of it's template (i.e. has page-create for the template) * * When this function is called, it has already been determined that the user has page-add permission. * So this is just narrowing down to make sure they have access on a template. * */ protected function addableTemplate(Page $page, User $user) { $has = false; if(count($page->template->childTemplates)) { // page's template defines specific templates for children // see if the user has access to one of them foreach($page->template->childTemplates as $id) { $template = $this->wire('templates')->get($id); if(!$template->useRoles) $template = $page->getAccessTemplate(); //if($user->hasPermission('page-edit', $template)) $has = true; if($user->hasPermission('page-create', $template)) $has = true; if($has) break; } } else if($page->id == $this->wire('config')->usersPageID && $user->hasPermission('user-admin')) { // user-admin permission implies create access to the 'user' template $has = true; } else { // page's template does not specify templates for children // so check to see if they have edit access to ANY template that can be used foreach($this->wire('templates') as $template) { // if($template->noParents) continue; if($template->parentTemplates && !in_array($page->template->id, $template->parentTemplates)) continue; // if($template->flags & Template::flagSystem) continue; //$has = $user->hasPermission('page-edit', $template); $has = $user->hasPermission('page-create', $template); if($has) break; } } return $has; } /** * Is the given page moveable (i.e. change parent)? * * Without arguments, it just checks that the user is allowed to move the page (not where they are allowed to) * Optionally specify a $parent page as the first argument to check if they are allowed to move to that parent. * */ public function moveable($event) { $page = $event->object; $moveable = $page->editable('parent'); if($moveable && count($event->arguments) && $event->arguments[0] instanceof Page) { $parent = $event->arguments[0]; $moveable = $parent->addable($page); } if($page->id == 1) $moveable = false; $event->return = $moveable; } /** * Is the given page sortable by the current user? * */ public function sortable($event) { $page = $event->object; $sortable = false; if($page->id > 1 && $page->editable() && $this->user->hasPermission('page-sort', $page->parent)) $sortable = true; $event->return = $sortable; } /** * Is the page publishable by the current user? * * A field name may optionally be specified as the first argument, in which case the field on that page will also be checked for access. * */ protected function publishable($event) { $user = $this->wire('user'); $event->return = true; if($user->isSuperuser()) return; $page = $event->object; // if page isn't editable, it certainly can't be publishable if(!$page->editable()) { $event->return = false; return; } // if there is no page-publish permission, then it's publishable $perm = wire('permissions')->get('page-publish'); if(!$perm->id) return; // if Page is a user, and user has user-admin permission, they can also publish the user if($page instanceof User && $user->hasPermission('user-admin')) return; // check if user has the permission assigned if($user->hasPermission('page-publish', $page)) return; // if we made it here, then page is not publishable $event->return = false; } /** * Can the user create pages from this template? * * Optional argument 1 may be a parent page for context, i.e. can we create a page with this parent. * public function createable($event) { $template = $event->object; $user = $this->fuel('user'); $createable = false; if($template->noParents) { $createable = false; } else if($user->isSuperuser()) { $createable = true; } else if($user->hasPermission('page-create', $template)) { $createable = true; } // check if a parent $page is provided as the first argument for additional access checking if($createable && isset($event->arguments[0]) && $event->arguments[0] instanceof Page) { $parent = $event->arguments[0]; if($parent->template->noChildren || (count($parent->template->childTemplates) && !in_array($template->id, $parent->template->childTemplates))) $createable = false; if($createable) $createable = $parent->addable(); } $event->return = $createable; } */ }