artabro/wire/modules/PagePermissions.module
2024-08-27 11:35:37 +02:00

1197 lines
40 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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);
}
*/
}