1
0
mirror of https://github.com/processwire/processwire.git synced 2025-08-12 17:54:44 +02:00

Refactoring of ProcessController and add support for access controlled methods that works with the existing moduleInfo['nav'][]['permission'] setting. Previously that permission was only used to determine whether to show the item in the nav, and you had to do your own separate permission check in the actual method implementation. This commit also moves all the WireException definitions to the main core/Exceptions file.

This commit is contained in:
Ryan Cramer
2018-09-14 11:02:30 -04:00
parent 590a69502c
commit 64680df68f
5 changed files with 181 additions and 90 deletions

View File

@@ -8,7 +8,7 @@
* This file is licensed under the MIT license
* https://processwire.com/about/license/mit/
*
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
* ProcessWire 3.x, Copyright 2018 by Ryan Cramer
* https://processwire.com
*
*/
@@ -20,21 +20,51 @@
class WireException extends \Exception {}
/**
* Triggered when access to a resource is not allowed
* Thrown when access to a resource is not allowed
*
*/
class WirePermissionException extends WireException {}
/**
* Triggered when a requested item does not exist and generates a fatal error
* Thrown when a requested page does not exist, or can be thrown manually to show the 404 page
*
*/
class Wire404Exception extends WireException {}
/**
* WireDatabaseException is the exception thrown by the Database class
*
* If you use this class without ProcessWire, change 'extends WireException' below to be just 'extends Exception'
* Thrown when ProcessWire is unable to connect to the database at boot
*
*/
class WireDatabaseException extends WireException {}
/**
* Thrown when cross site request forgery detected by SessionCSRF::validate()
*
*/
class WireCSRFException extends WireException {}
/**
* Thrown when a requested Process or Process method is requested that doesnt exist
*
*/
class ProcessController404Exception extends Wire404Exception { }
/**
* Thrown when the user doesnt have access to execute the requested Process or method
*
*/
class ProcessControllerPermissionException extends WirePermissionException { }
/**
* Thrown by PageFinder when an error occurs trying to find pages
*
*/
class PageFinderException extends WireException { }
/**
* Thrown by PageFinder when it detects an error in the syntax of a given page-finding selector
*
*/
class PageFinderSyntaxException extends PageFinderException { }

View File

@@ -1,8 +1,5 @@
<?php namespace ProcessWire;
class PageFinderException extends WireException { }
class PageFinderSyntaxException extends PageFinderException { }
/**
* ProcessWire PageFinder
*

View File

@@ -23,14 +23,15 @@
* @method Process headline(string $headline)
* @method Process browserTitle(string $title)
* @method Process breadcrumb(string $href, string $label)
* @method install()
* @method uninstall()
* @method upgrade($fromVersion, $toVersion)
* @method void install()
* @method void uninstall()
* @method void upgrade($fromVersion, $toVersion)
* @method Page installPage($name = '', $parent = null, $title = '', $template = 'admin', $extras = array()) #pw-internal
* @method int uninstallPage() #pw-internal
* @method string executeNavJSON(array $options = array()) #pw-internal @todo
* @method ready()
* @method setConfigData(array $data)
* @method void ready()
* @method void setConfigData(array $data)
* @method void executed($methodName) Hook called after a method has been executed in the Process
*
*/

View File

@@ -5,23 +5,11 @@
*
* Loads and executes Process Module instance and determines access.
*
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
* ProcessWire 3.x, Copyright 2018 by Ryan Cramer
* https://processwire.com
*
*/
/**
* Exception thrown when a requested Process or Process method is requested that doesn't exist
*
*/
class ProcessController404Exception extends Wire404Exception { }
/**
* Exception thrown when the user doesn't have access to execute the requested Process
*
*/
class ProcessControllerPermissionException extends WirePermissionException { }
/**
* A Controller for Process* Modules
*
@@ -70,6 +58,14 @@ class ProcessController extends Wire {
*/
protected $processMethodName;
/**
* Process verbose module info
*
* @var array
*
*/
protected $processInfo = array();
/**
* The prefix to apply to the Process name
*
@@ -159,7 +155,8 @@ class ProcessController extends Wire {
// verify that there is adequate permission to execute the Process
$permissionName = '';
$info = $this->wire('modules')->getModuleInfo($processName, array('verbose' => false));
$info = $this->wire('modules')->getModuleInfoVerbose($processName);
$this->processInfo = $info;
if(!empty($info['permission'])) $permissionName = $info['permission'];
$this->hasPermission($permissionName, true); // throws exception if no permission
@@ -186,8 +183,6 @@ class ProcessController extends Wire {
*
* Note: an empty permission name is accessible only by the superuser
*
* @todo: This may now be completely unnecessary since permission checking is built into Modules.php
*
* @param string $permissionName
* @param bool $throw Whether to throw an Exception if the user does not have permission
* @return bool
@@ -198,48 +193,113 @@ class ProcessController extends Wire {
$user = $this->wire('user');
if($user->isSuperuser()) return true;
if($permissionName && $user->hasPermission($permissionName)) return true;
if($throw) throw new ProcessControllerPermissionException("You don't have $permissionName permission");
if($throw) {
throw new ProcessControllerPermissionException(
sprintf($this->_('You do not have “%s” permission'), $permissionName)
);
}
return false;
}
/**
* Does user have permission for the given $method name in the current Process?
*
* @param string $method
* @param bool $throw Throw exception if not permission?
* @return bool
* @throws ProcessControllerPermissionException
*
*/
protected function hasMethodPermission($method, $throw = true) {
// i.e. executeHelloWorld => helloWorld
$urlSegment = $method;
if(strpos($method, 'execute') === 0) list(,$urlSegment) = explode('execute', $method, 2);
$urlSegment = $this->wire('sanitizer')->hyphenCase($urlSegment);
if(!$this->hasUrlSegmentPermission($urlSegment, $throw)) return false;
return true;
}
/**
* Does user have permission for the given urlSegment in the current Process?
*
* @param string $urlSegment
* @param bool $throw Throw exception if not permission?
* @return bool
* @throws ProcessControllerPermissionException
*
*/
protected function hasUrlSegmentPermission($urlSegment, $throw = true) {
if(empty($this->processInfo['nav']) || $this->wire('user')->isSuperuser()) return true;
$hasPermission = true;
$urlSegment = trim(strtolower($urlSegment), '.-_');
foreach($this->processInfo['nav'] as $navItem) {
if(empty($navItem['permission'])) continue;
$navSegment = strtolower(trim($navItem['url'], './'));
if(empty($navSegment)) continue;
if(strpos($navSegment, '/') !== false) list($navSegment,) = explode($navSegment, '/', 2);
$navSegmentAlt = str_replace('-', '', $navSegment);
if($urlSegment === $navSegment || $urlSegment === $navSegmentAlt) {
$hasPermission = $this->hasPermission($navItem['permission'], $throw);
break;
}
}
return $hasPermission;
}
/**
* Get the name of the method to execute with the Process
*
* @param Process @process
* @return string
* @throws ProcessControllerPermissionException
*
*/
public function getProcessMethodName(Process $process) {
$method = $this->processMethodName;
if(!$method) {
$forceFail = false;
$urlSegment1 = $this->wire('input')->urlSegment1;
$method = self::defaultProcessMethodName;
// urlSegment as given by ProcessPageView
$urlSegment1 = $this->input->urlSegment1;
if($urlSegment1 && !$this->user->isGuest()) {
if(strpos($urlSegment1, '-')) {
// urlSegment1 has multiple hyphenated parts: convert hello-world to HelloWorld
foreach(explode('-', $urlSegment1) as $v) $method .= ucfirst($v);
} else {
// just one part
$method .= ucfirst($urlSegment1);
$sanitizer = $this->wire('sanitizer');
if($this->processMethodName) {
// the method to use has been preset with the setProcessMethodName() function
$method = $this->processMethodName;
if($method !== self::defaultProcessMethodName) {
$this->hasMethodPermission($method);
}
} else if(strlen($urlSegment1) && !$this->wire('user')->isGuest()) {
// determine requested method from urlSegment1
// $urlSegment1 = trim($this->wire('sanitizer')->hyphenCase($urlSegment1, array('allow' => 'a-z0-9_')), '_');
if(ctype_alpha($urlSegment1)) {
$methodName = ucfirst($urlSegment1);
$hyphenName = $urlSegment1;
} else {
$methodName = trim($sanitizer->pascalCase($urlSegment1, array('allowUnderscore' => true)), '_');
$hyphenName = trim($sanitizer->hyphenCase($methodName, array('allowUnderscore' => true)), '_');
}
if($hyphenName != strtolower($urlSegment1) && strtolower($methodName) != strtolower($urlSegment1)) {
// if urlSegment changed from sanitization, likely not in valid format
$forceFail = true;
} else {
// valid
$method .= $methodName; // execute => executeHelloWorld
$this->hasUrlSegmentPermission($hyphenName);
}
}
if($forceFail) return '';
if($method === 'executed') return '';
$hookedMethod = "___$method";
if(method_exists($process, $method)) return $method;
if(method_exists($process, "___$method")) return $method;
if($process->hasHook($method . '()')) return $method;
if(method_exists($process, $method)
|| method_exists($process, $hookedMethod)
|| $process->hasHook($method . '()')) {
return $method;
} else {
return '';
}
}
/**
* Execute the process and return the resulting content generated by the process
@@ -250,44 +310,53 @@ class ProcessController extends Wire {
*/
public function ___execute() {
$content = '';
$method = '';
$debug = $this->wire('config')->debug;
$breadcrumbs = $this->wire('breadcrumbs');
$headline = $this->wire('processHeadline');
$numBreadcrumbs = $breadcrumbs ? count($breadcrumbs) : null;
if($process = $this->getProcess()) {
if($method = $this->getProcessMethodName($this->process)) {
$className = $this->process->className();
if($debug) Debug::timer("$className.$method()");
$content = $this->process->$method();
if($debug) Debug::saveTimer("$className.$method()");
if($method != 'execute') {
// some method other than the main one
if(!is_null($numBreadcrumbs) && $numBreadcrumbs === count($breadcrumbs)) {
// process added no breadcrumbs, but there should be more
if($headline === $this->wire('processHeadline')) $process->headline(str_replace('execute', '', $method));
$moduleInfo = $this->wire('modules')->getModuleInfo($process);
$href = substr($this->wire('input')->url(), -1) == '/' ? '../' : './';
$process->breadcrumb($href, $moduleInfo['title']);
$process = $this->getProcess();
if(!$process) {
throw new ProcessController404Exception("Process does not exist: $this->processError");
}
}
$this->process->executed($method);
} else {
// determine method (throws ProcessControllerPermissionException if no access)
$method = $this->getProcessMethodName($process);
if(!$method) {
throw new ProcessController404Exception("Unrecognized path");
}
} else {
throw new ProcessController404Exception("The requested process does not exist - $this->processError");
// call method from Process (and time it if debug mode enabled)
$className = $process->className();
if($debug) Debug::timer("$className.$method()");
$content = $process->$method();
if($debug) Debug::saveTimer("$className.$method()");
// setup breadcrumbs if in some method other than the main execute() method
if($method !== 'execute') {
// some method other than the main one
if(!is_null($numBreadcrumbs) && $numBreadcrumbs === count($breadcrumbs)) {
// process added no breadcrumbs, but there should be more
if($headline === $this->wire('processHeadline')) {
$process->headline(str_replace('execute', '', $method));
}
$href = substr($this->wire('input')->url(), -1) == '/' ? '../' : './';
$process->breadcrumb($href, $this->processInfo['title']);
}
}
// triggered "executed" (execute done) hook
$process->executed($method);
if(empty($content) || is_bool($content)) {
$content = $this->process->getViewVars();
$content = $process->getViewVars();
}
if(is_array($content)) {
// array of returned content indicates variables to send to a view
if(count($content) || $this->process->getViewFile()) {
$viewFile = $this->getViewFile($this->process, $method);
if(count($content) || $process->getViewFile()) {
$viewFile = $this->getViewFile($process, $method);
if($viewFile) {
// get output from a separate view file
$template = $this->wire(new TemplateFile($viewFile));

View File

@@ -8,12 +8,6 @@
*
*/
/**
* Exception triggered by SessionCSRF::validate() when CSRF detected
*
*/
class WireCSRFException extends WireException {}
/**
* ProcessWire CSRF Protection
*