1153 lines
39 KiB
Text
1153 lines
39 KiB
Text
<?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 2021 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(is_null($this->hasPageEditCreated)) {
|
||
$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(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->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 = $config->guestUserRolePageID;
|
||
|
||
$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, they don't have permission
|
||
if($processName !== 'ProcessUser' && (!$process instanceof ProcessPageList) && (!$process instanceof ProcessPageLister)) {
|
||
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]
|
||
$userAdminAll = $this->wire()->permissions->get('user-admin-all');
|
||
|
||
// if there are no special permissions, then let them through
|
||
if(!$userAdminAll->id) return true;
|
||
|
||
// if user has user-admin-all permission, they are good to edit
|
||
if($user->hasPermission($userAdminAll)) return true;
|
||
|
||
// there are role-specific permissions in the system, and user must have appropriate one to edit
|
||
$userEditable = false;
|
||
$n = 0;
|
||
foreach($page->roles as $role) {
|
||
$n++;
|
||
if($role->id == $guestRoleID) continue;
|
||
if($user->hasPermission("user-admin-$role->name")) {
|
||
// found a matching permission for role, so it is editable
|
||
$userEditable = true;
|
||
break;
|
||
}
|
||
}
|
||
if($userEditable) return true;
|
||
// if there is only role (guest), then no specific permission needed for that
|
||
if($n == 0 || ($n == 1 && $page->roles->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;
|
||
|
||
// cannot assign superuser role
|
||
if($role->id == $this->wire('config')->superUserRolePageID) return false;
|
||
|
||
// user with user-admin can always assign guest role
|
||
if($role->id == $this->wire('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;
|
||
$name = $_name;
|
||
$field = $this->wire('fields')->get($name);
|
||
}
|
||
if($field && $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(is_object($name) && $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(is_object($name) && $name instanceof Field) $name = $name->name;
|
||
if(empty($name) || !is_string($name)) return false;
|
||
if(is_null($user)) $user = $this->wire('user');
|
||
if(!$user->isLoggedin()) return false;
|
||
if(!$user->hasPermission('profile-edit')) return false;
|
||
$data = $this->wire('modules')->getModuleConfigData('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 isn’t 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 && $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) {
|
||
|
||
$page = $event->object;
|
||
$user = $this->wire('user');
|
||
$listable = true;
|
||
|
||
if($page instanceof NullPage) $listable = false;
|
||
else if($user->isSuperuser()) $listable = true;
|
||
else if($page instanceof User && $user->hasPermission('user-admin')) $listable = 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) $listable = 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) {
|
||
/** @var User $user */
|
||
$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;
|
||
/** @var User $user */
|
||
$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;
|
||
/** @var User $user */
|
||
$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 || !$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 || !$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) {
|
||
|
||
/** @var User $user */
|
||
$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);
|
||
}
|
||
*/
|
||
|
||
|
||
}
|