artabro/wire/modules/PagePermissions.module

1198 lines
40 KiB
Text
Raw Permalink Normal View History

2024-08-27 11:35:37 +02:00
<?php namespace ProcessWire;
/**
* ProcessWire Page Permissions Module
*
* Adds convenience methods to all Page objects for checking permissions, i.e.
*
* if($page->editable()) { do something }
* if(!$page->viewable()) { echo "sorry you can't view this"; }
* ...and so on...
*
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
* https://processwire.com
*
* Optional special permissions that are optional (by default, not installed):
*
* 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.
*
* 3. page-rename: when installed, user must have this permission in their role before they
* can change the name of a published page. They can still change the name of an unpublished
* page. When not installed, this permission falls back to page-edit.
*
* 4. page-edit-lang-default: when installed on a multi-language site, user must have this
* permission in order edit multi-language fields in "default" language. This permission
* is also required to create or delete pages (if user already has other permissions that enable
* them to create or delete pages).
*
* 5. page-edit-lang-[language_name]: when installed on a multi-language site, user must have
* this permission to edit multi-language fields in the [language_name] language.
*
* 6. page-edit-lang-none: when installed on a multi-language site, user must have this
* permission to edit non-multi-language fields. They must also have it to create or delete
* pages (if user already has other permissions that enable that).
*
* 7. user-admin-all: when installed, a user must have this permission in order to edit other
* users of all roles (except superuser of course). When installed, the regular user-admin
* permission only acts as a pre-requisite for editing users, and enables only listing users,
* and editing users that have only the 'guest' role. The user-admin-all essentially grants
* the same thing as user-admin permission does on a system that has no user-admin-all
* permission installed. That's what it does, but why is it here? The entire purpose is to
* support user-admin-[role] permissions, as described in the next item below:
*
* 8. user-admin-[role_name]: when installed on a site that also has user-admin-all permission
* installed, a user must have this permission (along with user-admin permission) in order
* to edit users in role [role_name], or to grant that role to other guest users, or to
* revoke it from them. Think of this permission as granting a user administrative privileges
* to just a specific group of users. Note that there would be no reason to combine this
* permission with the user-admin-all permission, as user-admin-all permission grants admin
* privileges to all roles.
*
*/
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;
/**
* Array of optional permission name => boolean of whether the system has it
*
* @var array
*
*/
protected $hasOptionalPermissions = array();
/**
* 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::restorable', $this, 'restorable');
$this->addHook('Page::addable', $this, 'addable');
$this->addHook('Page::moveable', $this, 'moveable');
$this->addHook('Page::sortable', $this, 'sortable');
// $this->addHook('Page::fieldViewable', $this, 'hookFieldViewable');
// $this->addHook('Page::fieldEditable', $this, 'hookFieldEditable');
// $this->addHook('Template::createable', $this, 'createable');
}
/**
* Hook that adds a Page::editable([$field]) method to determine if $page is editable by 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.
*
* When using field, specify boolean false for second argument to bypass PageEditable check, as an
* optimization, if you have already determined that the page is editable.
*
* @param HookEvent $event
*
*/
public function editable($event) {
/** @var Page $page */
$page = $event->object;
$field = $event->arguments(0);
$checkPageEditable = true;
if($field && $event->arguments(1) === false) $checkPageEditable = false;
if($checkPageEditable && !$this->pageEditable($page)) {
$event->return = false;
} else if($field) {
$event->return = $this->fieldEditable($page, $field, false);
} else {
$event->return = true;
}
}
/**
* Is the given page editable?
*
* @param Page $page
* @return bool
*
*/
public function pageEditable(Page $page) {
if($this->wire()->hooks->isHooked('PagePermissions::pageEditable()')) {
return $this->__call('pageEditable', array($page));
} else {
return $this->___pageEditable($page);
}
}
/**
* Hookable implmentation for: Is the given page editable?
*
* @param Page $page
* @return bool
*
*/
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) && $page->template != 'language') 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", $page)) return false;
}
// special conditions apply if the page is a Language
if($page->template == 'language' && $user->hasPermission('page-edit') && $user->hasPermission('lang-edit')) {
return $this->hasLangPermission("page-edit-lang-$page->name", $user);
}
// special conditions apply if the page is a User
if($page instanceof User || in_array($page->template->id, $this->wire()->config->userTemplateIDs)) {
return $this->userEditable($page);
}
// 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($this->hasPageEditCreated === null) {
$this->hasPageEditCreated = $this->wire()->permissions->has('page-edit-created');
}
if($this->hasPageEditCreated && $user->hasPermission('page-edit-created', $page)) {
// 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($this->hasPagePublish === null) {
$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->hasStatus(Page::statusUnpublished)) return true;
// otherwise user cannot edit this page
return false;
}
return true;
}
/**
* Returns whether the given user ($page) is editable by the current user
*
* @param User|Page $page
* @param array $options
* - `viewable` (bool): Specify true if only a viewable check is needed (default=false)
* @return bool
*
*/
public function userEditable(Page $page, array $options = array()) {
/** @var Process|ProcessProfile|ProcessPageView|ProcessUser|ProcessPageList|ProcessPageLister $process */
$user = $this->wire()->user;
$process = $this->wire()->process;
$processName = (string) $process;
$config = $this->wire()->config;
$guestRoleID = (int) $config->guestUserRolePageID;
$permissions = $this->wire()->permissions;
$defaults = array(
'viewable' => false, // specify true if method is being used to determine viewable state
);
$options = count($options) ? array_merge($defaults, $options) : $defaults;
if(!$page->id) return false;
if(!$page instanceof User) {
$template = $this->wire()->templates->get('user');
if($page->className() !== $template->getPageClass(false)) {
$page = $this->wire()->users->get($page->id);
}
}
if(!$page || $page instanceof NullPage) return false;
if($user->id === $page->id && $user->isLoggedin()) {
// user is the same as the page being edited or viewed
$wirePage = $this->wire()->page;
$wireProcessName = $wirePage ? "$wirePage->process" : '';
if(($processName === 'ProcessProfile' || $wireProcessName === 'ProcessProfile') && $user->hasPermission('profile-edit')) {
// user editing themself in ProcessProfile
return true;
}
if($options['viewable'] && $user->hasPermission('user-view-self')) {
// user requests to view themselves
return true;
}
}
if($options['viewable']) {
// perform viewable checks rather than editable checks
if($page->template->useRoles && $user->hasPermission('page-view', $page)) {
// access permission provided by page-view permission configured directly (not inherited) on user template
if($user->isLoggedin()) return true;
}
if($user->hasPermission('user-view-all')) {
// user-view-all permission is similar to page-view on the user template but can also be assigned to guest role
return true;
}
if($page->isSuperuser()) {
// if superuser role is present then view permission cannot be provided by any other user-view-[role]
return $user->isSuperuser() || $user->hasPermission('user-view-superuser');
}
// check for match between user-view-[role] permission and roles of user being requested
$userViewable = false;
foreach($page->roles as $role) {
// check for "user-view-[roleName]" permissions
if($role->id == $guestRoleID) continue;
if($user->hasPermission("user-view-$role->name")) $userViewable = true;
if($userViewable) break;
}
if($userViewable) return true;
} else {
// if the current process is something other than ProcessUser (or a Process module that can be
// used within ProcessUser) they don't have permission
$processNames = array(
'ProcessUser',
'ProcessPageList',
'ProcessPageLister',
'ProcessPageEditImageSelect'
);
if(!wireInstanceOf($process, $processNames)) return false;
}
// if user doesn't have user-admin permission, they have no edit access
if(!$user->hasPermission('user-admin')) 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
$superuserRole = $this->wire()->roles->get($config->superUserRolePageID);
if($page->roles->has($superuserRole) && !$user->roles->has($superuserRole)) return false;
// if we reach this point then check if there are more granular user-admin permissions available
// special permissions: user-admin-all, and user-admin-[role]
$userAdminPerms = $permissions->getPermissionNameIds('user-admin-');
// if there are no special permissions, then let them through
if(isset($userAdminPerms['user-admin-all'])) {
// if user has 'user-admin-all' permission, they are good to edit
if($user->hasPermission('user-admin-all')) return true;
// if there are no other user-admin perms then not editable
if(count($userAdminPerms) === 1) return false;
} else if(empty($userAdminPerms)) {
// no 'user-admin-[role]' permissions means permission delegated to just 'user-admin'
return true;
}
// there are role-specific permissions in the system, and user must have appropriate one to edit
$userEditable = false;
$pageRoles = $page->roles;
$n = 0;
foreach($pageRoles as $role) {
$n++;
if($role->id == $guestRoleID) continue;
$permName = "user-admin-$role->name";
if(!isset($userAdminPerms[$permName])) continue; // does not exist
if($user->hasPermission($permName)) {
// found a matching permission for role, so it is editable
$userEditable = true;
break;
} else {
// user does not have
}
}
if($userEditable) return true;
// if there is only 1 role (guest), then no role-specific permission needed for that
if($n == 0 || ($n == 1 && $pageRoles->first()->id == $guestRoleID)) return true;
return false;
}
/**
* Returns whether the given user ($page) is viewable by the current user
*
* @param User|Page $page
* @param array $options
* @return bool
* @throws WireException
*
*/
public function userViewable(Page $page, array $options = array()) {
$user = $this->wire()->user;
// user viewing themself
if($user->id === $page->id && $user->hasPermission('page-view', $page)) return true;
$options['viewable'] = true;
return $this->userEditable($page, $options);
}
/**
* Can the current user add/remove the given role from other users?
*
* @param string|Role|int $role
* @return bool
*
*/
public function userCanAssignRole($role) {
$user = $this->wire()->user;
// superuser can assign any role
if($user->isSuperuser()) return true;
// user-admin permission is a pre-requisite for any kind of role assignment
if(!$user->hasPermission('user-admin')) return false;
// make sure we have a Role object
if(!$role instanceof Role) $role = $this->wire()->roles->get($role);
if(!$role->id) return false;
$config = $this->wire()->config;
// cannot assign superuser role
if($role->id == $config->superUserRolePageID) return false;
// user with user-admin can always assign guest role
if($role->id == $config->guestUserRolePageID) return true;
// check for user-admin-all permission
$userAdminAll = $this->wire()->permissions->get('user-admin-all');
if(!$userAdminAll->id) {
// if there is no user-admin-all permission, then user-admin permission is
// all that is necessary to edit other users of any roles except superuser
return true;
}
if($user->hasPermission($userAdminAll)) {
// if user has user-admin-all permission, then they can assign any roles except superuser
return true;
}
// return whether user has permission specific to role
return $user->hasPermission("user-admin-$role->name");
}
/**
* Is the given Field or field name viewable?
*
* This provides the implementation for the Page::fieldViewable($field) method.
*
* @param Page $page
* @param string|Field $name Field name
* @param bool $checkPageViewable Check if the page is viewable? Default=true.
* Specify false here as an optimization if you've already confirmed $page is viewable.
* @return bool
*
*/
public function fieldViewable(Page $page, $name, $checkPageViewable = true) {
if(empty($name)) return false;
if($checkPageViewable && !$page->viewable(false)) {
return false;
}
if(is_object($name)) {
if($name instanceof Field) {
// Field object
$field = $name;
$name = $field->name;
} else {
// objects that aren't fields aren't viewable
return false;
}
} else {
$_name = $this->wire()->sanitizer->fieldName($name);
// if field name doesn't follow known format, return false
if($_name !== $name) return false;
$field = $this->wire()->fields->get($name);
}
if($field instanceof Field) {
// delegate to Field::viewable method
return $field->useRoles ? $field->viewable($page) : true;
} else if($this->wire($name)) {
// API vars are not viewable
return false;
}
// something else that we don't consider access for
return true;
}
/**
* Is the given field name editable?
*
* This provides the implementation for the Page::fieldEditable($field) method.
*
* @param Page $page
* @param string|Field $name Field name
* @param bool $checkPageEditable Check if the page is editable? Default=true.
* Specify false here as an optimization if you've already confirmed $page is editable.
* @return bool
*
*/
public function fieldEditable(Page $page, $name, $checkPageEditable = true) {
if(empty($name)) return false;
if($checkPageEditable && !$this->pageEditable($page)) return false;
if($name instanceof Field) $name = $name->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 === 'name') {
// if page has no name (and not homepage), then it needs one, so it is allowed
if($page->id > 1 && !strlen($page->name)) return true;
// if page is not yet published, user with page-edit can still change name
if($page->isUnpublished()) return true;
// otherwise verify page-rename permission
return $user->hasPermission('page-rename', $page);
}
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;
}
if($user->id === $page->id && !$user->isSuperuser() && !$user->hasPermission('user-admin')) {
return $this->userFieldEditable($name, $user);
}
// check per-field edit access
$field = $this->wire()->fields->get($name);
if($field && $field->useRoles) {
return $field->editable($page);
}
return true;
}
/**
* Is given file viewable?
*
* It is assumed that you have already determined the Page is viewable.
*
* @param Page $page
* @param Pagefile|string $pagefile
* @return bool|null Returns bool, or null if not known
* @since 3.0.166
*
*/
protected function fileViewable(Page $page, $pagefile) {
if($this->wire()->user->isSuperuser()) return true;
if(!$pagefile instanceof Pagefile) {
$pagefile = $page->hasFile(basename($pagefile), array('getPagefile' => true));
if(!$pagefile) return null;
}
$field = $pagefile->field;
if(!$field) return null;
return $this->fieldViewable($page, $field, false);
}
/**
* Is the given field editable by the current user in their user profile?
*
* @param Field|string $name Field or Field name
* @param User|null User to check (default=current user)
* @return bool
*
*/
public function userFieldEditable($name, User $user = null) {
if($name instanceof Field) $name = $name->name;
if(empty($name) || !is_string($name)) return false;
if($user === null) $user = $this->wire()->user;
if(!$user->isLoggedin()) return false;
if(!$user->hasPermission('profile-edit')) return false;
$data = $this->wire()->modules->getConfig('ProcessProfile');
$profileFields = isset($data['profileFields']) ? $data['profileFields'] : array();
if(in_array($name, $profileFields)) return true;
return false;
}
/**
* Hook for Page::viewable() or Page::viewable($user) method
*
* Is the page viewable by the current user? (or specified user)
*
* - Optionally specify User object to hook as first argument to check for a specific User.
* - Optionally specify a field name (or Field object) as first argument to check for specific field.
* - Optionally specify Language object or language name as first argument to check if viewable
* in that language (requires LanguageSupportPageNames module).
* - Optionally specify boolean false as first or second argument to bypass template filename check.
* - Optionally specify a Pagefile object or file basename to check if file is viewable. (3.0.166+)
*
* Returns boolean true or false. If given a Pagefile or file basename, it can also return null if
* the Page itself is viewable but the file did not map to something we recognize as access controlled,
* like a file basename that isnt present in any file fields on the page.
*
* @param HookEvent $event
*
*/
public function viewable($event) {
/** @var Page $page */
$page = $event->object;
$viewable = true;
$user = $this->wire()->user;
$arg0 = $event->arguments(0);
$arg1 = $event->arguments(1);
$field = null; // field name or Field object, if specified as arg0
$checkTemplateFile = true; // return false if template filename doesn't exist
$pagefile = null;
$status = $page->status;
// allow specifying User instance as argument 0
// this gives you a "viewable to user" capability
if($arg0) {
if($arg0 instanceof User) {
// user specified
$user = $arg0;
} else if($arg0 instanceof Pagefile || (is_string($arg0) && strpos($arg0, '.'))) {
// Pagefile or file basename
$pagefile = $arg0;
$checkTemplateFile = false;
} else if($arg0 instanceof Field || is_string($arg0)) {
// field name, Field object or language name specified
// @todo: prevent possible collision of field name and language name
$field = $arg0;
$checkTemplateFile = false;
}
}
if($arg0 === false || $arg1 === false) {
// bypass template filename check
$checkTemplateFile = false;
}
// if page has corrupted status, this need not affect viewable access
if($status & Page::statusCorrupted) $status = $status & ~Page::statusCorrupted;
// perform several viewable checks, in order
if($status >= Page::statusUnpublished) {
// unpublished pages are not viewable, but see override below this if/else statement
$viewable = false;
} else if(!$page->template || ($checkTemplateFile && !$page->template->filenameExists())) {
// template file does not exist
$viewable = false;
} else if($user->isSuperuser()) {
// superuser always allowed
// $viewable = true;
} else if($page->hasField('process') && $page->get('process')) {
// delegate access to permissions defined with Process module
$viewable = $this->processViewable($page);
} else if($page instanceof User) { // && !$user->isGuest() && ($user->hasPermission('user-admin') || $page->id === $user->id)) {
// user administrator or user viewing themself
$viewable = $this->userViewable($page);
} else if(!$user->hasPermission("page-view", $page)) {
// user lacks basic view permission to page
$viewable = false;
} else if($page->isTrash()) {
// pages in trash are not viewable, except to superuser
$viewable = false;
}
// if the page is editable by the current user, force it to be viewable (if not viewable due to being unpublished)
if(!$viewable && !$user->isGuest() && ($status & Page::statusUnpublished)) {
if($page->editable() && (!$checkTemplateFile || $page->template->filenameExists())) $viewable = true;
}
if($field && $viewable) {
$viewable = $this->fieldViewable($page, $field, false);
} else if($pagefile) {
if(!$viewable && wireInstanceOf($page, 'RepeaterPage')) {
/** @var RepeaterPage $page */
$viewable = $page->getForPageRoot()->viewable($checkTemplateFile);
}
if($viewable) {
$viewable = $this->fileViewable($page, $pagefile);
}
}
$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.
*
* @param Page $page
* @return bool
*
*/
protected function processViewable(Page $page) {
$user = $this->wire()->user;
$process = $page->process;
if($user->isGuest()) return false;
if($user->isSuperuser()) return true;
return $this->wire()->modules->hasPermission($process, $user, $page, true);
}
/**
* 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.
*
* @param HookEvent $event
*
*/
public function listable($event) {
/** @var Page $page */
$page = $event->object;
$user = $this->wire()->user;
$listable = true;
if($page instanceof NullPage) {
$listable = false;
} else if($user->isSuperuser()) {
// true
} else if($page instanceof User && $user->hasPermission('user-admin')) {
// true
} else if($page->hasStatus(Page::statusUnpublished) && !$page->editable()) {
$listable = false;
} else if($page->process && !$this->processViewable($page)) {
$listable = false;
} else if($page->isTrash()) {
$listable = $this->trashListable($page);
} else if(($accessTemplate = $page->getAccessTemplate()) && $accessTemplate->guestSearchable) {
// true
} else if(!$user->hasPermission("page-view", $page)) {
$listable = false;
}
$event->return = $listable;
}
/**
* Return whether or not given page in Trash is listable
*
* @param Page|null $page Page, or specify null for a general "trash is listable" request
* @return bool
*
*/
public function trashListable($page = null) {
$user = $this->wire()->user;
// trash and anything in it always visible to superuser
if($user->isSuperuser()) return true;
// determine if system has page-edit-trash-created permission installed
$petc = 'page-edit-trash-created';
if(!$this->wire()->permissions->has($petc)) $petc = false;
if($user->hasPermission('page-delete')) {
// has page-delete globally
} else if($petc && $user->hasPermission($petc)) {
// has page-edit-trash-created globally
} else if($user->hasPermission('page-delete', true)) {
// has page-delete added specifically at a template
} else if($petc && $user->hasPermission($petc, true)) {
// has page-edit-trash-created added specifically at a template
} else {
// user does not have any of the permissions above, so trash is not listable
return false;
}
// if request not asking about specific page, return general "trash is listable?" request
if($page === null || !$page->id) return true;
// if request is for the actual Trash page, consider this to be a general request
if($page->id == $this->wire()->config->trashPageID) return true;
// page is listable in the trash only if it is also editable
return $this->pageEditable($page);
}
/**
* Is the page deleteable by the current user?
*
* @param HookEvent $event
*
*/
public function deleteable($event) {
/** @var Page $page */
$page = $event->object;
$user = $this->wire()->user;
if($page->isLocked()) {
$deleteable = false;
} else if($page instanceof User && $user->hasPermission('user-admin')) {
/** @var User $page */
$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);
}
if($deleteable && $this->wire()->languages) {
// in multi-language environment, if user can't edit default language or can't edit non-multi-language fields,
// then deny access to delete the page
if(!$this->hasPageEditLangDefault($user, $page) || !$this->hasPageEditLangNone($user, $page)) {
$deleteable = false;
}
}
}
$event->return = $deleteable;
}
/**
* Is the page trashable by the current user?
*
* Optionally specify boolean true for first argument to make this method behave as: "Is page deleteable OR trashable?"
*
* @param HookEvent $event
*
*/
public function trashable($event) {
/** @var Page $page */
$page = $event->object;
$event->return = false;
if($event->arguments(0) !== true) {
if($page->hasStatus(Page::statusTrash) || $page->template->noTrash) {
// if page is already in trash, or template doesn't allow placement in trash, we return false
return;
}
}
if(!$page->isLocked()) {
$this->deleteable($event);
if(!$event->return && $this->wire()->permissions->has('page-edit-trash-created') && $page->editable()) {
// page can be trashable if user created it
$user = $this->wire()->user;
$trashable = ($page->created_users_id === $user->id && $user->hasPermission('page-edit-trash-created', $page));
$event->return = $trashable;
}
}
}
/**
* Is page restorable from trash?
*
* @param HookEvent $event
*
*/
public function restorable($event) {
/** @var Page $page */
$page = $event->object;
$user = $this->wire()->user;
$event->return = false;
if($page->isLocked()) return;
if(!$page->isTrash() && !$page->rootParent()->isTrash()) return;
if(!$user->isSuperuser() && !$page->editable()) return;
$info = $this->wire()->pages->trasher()->getRestoreInfo($page);
if(!$info['restorable']) return;
/** @var Page $parent */
$parent = $info['parent'];
// check if parent does not allow this user to add pages here
if(!$parent->id || !$parent->addable($page)) return;
$event->return = true;
}
/**
* 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))
*
* @param HookEvent $event
*
*/
public function addable($event) {
/** @var Page $page */
$page = $event->object;
$user = $this->wire()->user;
$addable = false;
$addPage = null;
$_ADDABLE = false; // if we really mean it (as in, do not perform secondary checks)
$superuser = $user->isSuperuser();
if($page->template && $page->template->noChildren) {
// addable=false
} else if($superuser) {
$addable = true;
$_ADDABLE = true;
} else if(in_array($page->id, $this->wire()->config->usersPageIDs) && $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;
$_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) {
$addPage = $event->arguments(0);
if(!$addPage instanceof Page || !$addPage->id) $addPage = null;
if($addPage && $addPage->template && $page->template) {
if(count($page->template->childTemplates) && !in_array($addPage->template->id, $page->template->childTemplates)) {
$addable = false;
}
}
}
// check additional permissions if in multi-language environment
if($addable && !$_ADDABLE && $addPage && $this->wire()->languages) {
if(!$this->hasPageEditLangDefault($user, $addPage) || !$this->hasPageEditLangNone($user, $addPage)) {
// if user can't edit default language, or can't edit non-multi-language fields, then deny add access
$addable = false;
}
}
$event->return = $addable;
}
/**
* Checks that a parent is addable within the context of its 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.
*
* @param Page $page
* @param User $user
* @return bool
*
*/
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('edit');
if($template && $user->hasPermission('page-create', $template)) $has = true;
if($has) break;
}
} else if(in_array($page->id, $this->wire()->config->usersPageIDs) && $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.
*
* @param HookEvent $event
*
*/
public function moveable($event) {
/** @var Page $page */
$page = $event->object;
/** @var Page|null $parent */
$parent = $event->arguments(0);
if(!$parent instanceof Page || !$parent->id) $parent = null;
if($page->id == 1) {
$moveable = false;
} else {
$moveable = $page->editable('parent');
}
if($moveable && $parent) {
$moveable = $parent->addable($page);
} else if($parent && $parent->isTrash() && $parent->id == $this->wire()->config->trashPageID) {
$moveable = $page->deletable();
}
$event->return = $moveable;
}
/**
* Is the given page sortable by the current user?
*
* @param HookEvent $event
*
*/
public function sortable($event) {
/** @var Page $page */
$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.
*
* @param HookEvent $event
*
*/
public function publishable($event) {
$user = $this->wire()->user;
$event->return = true;
if($user->isSuperuser()) return;
/** @var Page $page */
$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
$hasPublish = $this->wire()->permissions->has('page-publish');
if(!$hasPublish) 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;
}
/**
* Returns true if given user has the optional language permission, or false if not
*
* In a non-multi-language system, this method will always return true.
* In a multi-language system that doesn't have the permission installed, this always returns true.
* In a multi-language system that DOES have it installed, methods returns true when user has it via one of their roles.
* This method assumes the user is already known to have any pre-requisite permissions.
*
* @param string $name Permission name i.e. page-edit-lang-none, page-edit-lang-default, etc.
* @param User $user
* @param Page|Template $context Optional Page or Template context
* @return bool
*
*/
protected function hasLangPermission($name, User $user, $context = null) {
if($user->isSuperuser()) return true;
if(!array_key_exists($name, $this->hasOptionalPermissions)) {
if($this->wire()->languages) {
$this->hasOptionalPermissions[$name] = $this->wire()->permissions->get($name)->id > 0;
} else {
// not applicable since multi-language not installed
$this->hasOptionalPermissions[$name] = false;
}
}
if($this->hasOptionalPermissions[$name]) {
// now check if the user has this permission
return $user->hasPermission($name, $context);
} else {
// system doesn't need to consider this permission
return true;
}
}
/**
* Returns true if given user is allowed to edit values in default language
*
* @param User $user
* @param Page|Template $context
* @return bool
*
*/
protected function hasPageEditLangDefault(User $user, $context = null) {
return $this->hasLangPermission('page-edit-lang-default', $user, $context);
}
/**
* Returns true if given user is allowed to edit non-multi-language values
*
* @param User $user
* @param Page|Template $context
* @return bool
*
*/
protected function hasPageEditLangNone(User $user, $context = null) {
return $this->hasLangPermission('page-edit-lang-none', $user, $context);
}
/**
* 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;
}
*/
/**
* Hook for Page::fieldViewable($field) method
*
* @param HookEvent $event
* @return bool|null
*
public function hookFieldViewable(HookEvent $event) {
$field = $event->arguments(0);
$page = $event->object;
$event->return = $this->fieldViewable($page, $field);
}
*/
/**
* Hook for Page::fieldEditable($field) method
*
* @param HookEvent $event
*
public function hookFieldEditable(HookEvent $event) {
$field = $event->arguments(0);
$page = $event->object;
$event->return = $this->fieldEditable($page, $field);
}
*/
}