<?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 2025 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();

	/**
	 * Runtime caches
	 * 
	 * @var array 
	 * 
	 */
	protected $caches = 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::cloneable', $this, 'cloneable');
		// $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;
		}

		/*
		 * Potential addition (cc:@adrianbj)
		 * cancel editable if page in trash, not restorable, and trash page not listable? 
		if($page->isTrash() && !$page->restorable()) {
			$trashId = $this->wire()->config->trashPageID;
			if($page->id == $trashId) return false;
			$trashPage = $this->wire()->pages->get($trashId);
			if(!$trashPage->listable()) return false;
		}
		*/

		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 page ($page) is editable by the current user 
	 * 
	 * @param Page $page
	 * @param array $options
	 *  - `viewable` (bool): Specify true if only a viewable check is needed (default=false)
	 *  - `processName` (string): Name of Process module to consider for context. 3.0.244+ (default='')
	 * @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;
		$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
			'processName' => '', 
		);
		
		$options = count($options) ? array_merge($defaults, $options) : $defaults;
		$processName = $options['processName'] ? $options['processName'] : (string) $process;
	
		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 page ($page) is viewable by the current user
	 * 
	 * @param Page $page
	 * @param array $options 
	 *   - `processName` (string): Name of Process module to consider for context. 3.0.244+ (default='')
	 * @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) {
		$user = $this->wire()->user;
		if($user->isSuperuser()) return true;
		if(!$pagefile instanceof Pagefile) {
			$of = $page->of();
			if($of) $page->of(false); // false so that temp files are considered 
			$pagefile = $page->hasFile(basename($pagefile), array('getPagefile' => true));
			if($of) $page->of(true);
			if(!$pagefile) return null;
		}
		$field = $pagefile->field;
		if(!$field) return null;
		if($pagefile->isTemp()) {
			// temporary file, just uploaded but not yet saved
			if(!$user->isLoggedin()) return false;
			if($page->id === $user->id) return $this->userFieldEditable($field, $user);
			return $this->fieldEditable($page, $field);
		}
		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+)
	 * - Optionally specify array as first or second argument for options (3.0.244+ internal use)
	 * 
	 * 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;
		$options = array();

		// 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;
			} else if(is_array($arg0)) {
				$options = $arg0;
			}
		} 
		
		if($arg1) {
			if(is_array($arg1) && empty($options)) $options = $arg1;
		}
		
		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, $options); 
		} 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 || $page->isLocked()) {
			$moveable = false;
		} else {
			$moveable = $page->editable('parent');
		}
	
		if($parent) {
			// parent specified
			if($moveable) {
				$moveable = $parent->addable($page);
			} else if($parent->isTrash() && $parent->id == $this->wire()->config->trashPageID) {
				$moveable = $page->deletable();
			}
		} else if($moveable) {
			// moveable but no parent specified
			// check to see if there is more than one parent it can move to
			$parentTemplates = $page->template->parentTemplates;
			if(empty($parentTemplates)) {
				// any parent that accepts any children is allowed
			} else {
				// only specific parents are allowed
				$templatesStr = implode('|', $parentTemplates);
				$cacheKey  = "moveable.$templatesStr";
				if(isset($this->caches[$cacheKey])) {
					$moveable = $this->caches[$cacheKey];
				} else {
					$pages = $this->wire()->pages;
					$s = "include=unpublished, id!=$page->parent_id, templates_id=$templatesStr";
					$moveable = $pages->count($s) > 0;
					$this->caches[$cacheKey] = $moveable;
				}
			}
		}
		
		$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; 
	}

	/**
	 * Is page cloneable by current user?
	 * 
	 * Note: If a page has children and current user is not superuser, this method will return
	 * true only if the user has the `page-clone-tree` permission (from ProcessPageClone module). 
	 * 
	 * ~~~~~
	 * if($page->cloneable()) {
	 *   // page is cloneable
	 * }
	 * if($page->cloneable(false)) {
	 *   // page is cloneable, ignoring if recursive clone of children is allowed
	 * }
	 * ~~~~~
	 * 
	 * @param HookEvent $event
	 * 
	 */
	public function cloneable($event) {
		
		$page = $event->object; /** @var Page $page */
		$recursive = $event->arguments(0); /** @var bool|null $recursive */
		$user = $this->wire()->user;
		$permissions = $this->wire()->permissions;
		$parent = $page->parent();
		$parentTemplate = $parent->template;
		$pageTemplate = $page->template;
		$event->return = false;

		if(!$parentTemplate) return;

		if($page->hasStatus(Page::statusSystem) || $page->hasStatus(Page::statusSystemID)) return;
		if($parentTemplate->noChildren) return;
		if($pageTemplate->noParents) return;

		if(count($parentTemplate->childTemplates) && !in_array($pageTemplate->id, $parentTemplate->childTemplates)) return;
		if(count($pageTemplate->parentTemplates) && !in_array($parentTemplate->id, $pageTemplate->parentTemplates)) return;

		if($user->isSuperuser()) {
			$event->return = true;
			return;
		}
		
		if(!$user->hasPermission('page-create', $page)) return;
		
		if(!$parent->addable()) return;
		
		if($permissions->has('page-clone')) {
			if(!$user->hasPermission('page-clone', $page)) return;
		}

		if($recursive === false || !$page->numChildren) {
			// call indicates they do not intend to clone children
			// or there are no children, so no additional checks necessary	
		} else {
			// check that they have page-clone-tree permission IF the page has children
			// recursive cloning is only allowed via ProcessPageClone module
			if(!$user->hasPermission('page-clone-tree', $page)) return;
		}
	
		$event->return = true;
	}
	
	/**
	 * 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);
	}
	 */


}
