514 lines
15 KiB
PHP
514 lines
15 KiB
PHP
|
<?php namespace ProcessWire;
|
||
|
|
||
|
/**
|
||
|
* ProcessWire ProcessController
|
||
|
*
|
||
|
* Loads and executes Process Module instance and determines access.
|
||
|
*
|
||
|
* ProcessWire 3.x, Copyright 2022 by Ryan Cramer
|
||
|
* https://processwire.com
|
||
|
*
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* A Controller for Process* Modules
|
||
|
*
|
||
|
* Intended to be used by templates that call upon Process objects
|
||
|
*
|
||
|
* @method string execute()
|
||
|
*
|
||
|
*/
|
||
|
class ProcessController extends Wire {
|
||
|
|
||
|
/**
|
||
|
* The default method called upon when no method is specified in the request
|
||
|
*
|
||
|
*/
|
||
|
const defaultProcessMethodName = 'execute';
|
||
|
|
||
|
/**
|
||
|
* The Process instance to execute
|
||
|
*
|
||
|
* @var Process
|
||
|
*
|
||
|
*/
|
||
|
protected $process;
|
||
|
|
||
|
/**
|
||
|
* The name of the Process to execute (string)
|
||
|
*
|
||
|
* @var string
|
||
|
*
|
||
|
*/
|
||
|
protected $processName;
|
||
|
|
||
|
/**
|
||
|
* Error message if unable to load Process module
|
||
|
*
|
||
|
* @var string
|
||
|
*
|
||
|
*/
|
||
|
protected $processError = '';
|
||
|
|
||
|
/**
|
||
|
* The name of the method to execute in this process
|
||
|
*
|
||
|
* @var string
|
||
|
*
|
||
|
*/
|
||
|
protected $processMethodName;
|
||
|
|
||
|
/**
|
||
|
* Process verbose module info
|
||
|
*
|
||
|
* @var array
|
||
|
*
|
||
|
*/
|
||
|
protected $processInfo = array();
|
||
|
|
||
|
/**
|
||
|
* The prefix to apply to the Process name
|
||
|
*
|
||
|
* All related Processes would use the same prefix, i.e. "Admin"
|
||
|
*
|
||
|
* @var string
|
||
|
*
|
||
|
*/
|
||
|
protected $prefix;
|
||
|
|
||
|
/**
|
||
|
* Construct the ProcessController
|
||
|
*
|
||
|
*/
|
||
|
public function __construct() {
|
||
|
parent::__construct();
|
||
|
$this->prefix = 'Process';
|
||
|
$this->processMethodName = ''; // blank indicates default/index method
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Set the Process to execute.
|
||
|
*
|
||
|
* @param Process $process
|
||
|
*
|
||
|
*/
|
||
|
public function setProcess(Process $process) {
|
||
|
$this->process = $process;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Set the name of the Process to execute.
|
||
|
*
|
||
|
* No need to call this unless you want to override the one auto-determined from the URL.
|
||
|
*
|
||
|
* If overridden, then make sure the name includes the prefix, and don't bother calling the setPrefix() method.
|
||
|
*
|
||
|
* @param string $processName
|
||
|
*
|
||
|
*/
|
||
|
public function setProcessName($processName) {
|
||
|
$processName = (string) $processName;
|
||
|
if(!ctype_alnum($processName)) {
|
||
|
$processName = $this->wire()->sanitizer->className($processName);
|
||
|
}
|
||
|
$this->processName = $processName;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Set the name of the method to execute in the Process
|
||
|
*
|
||
|
* It is only necessary to call this if you want to override the default behavior.
|
||
|
* The default behavior is to execute a method called "execute()" OR "executeSegment()" where "Segment" is
|
||
|
* the last URL segment in the request URL.
|
||
|
*
|
||
|
* @param string $processMethod
|
||
|
*
|
||
|
*/
|
||
|
public function setProcessMethodName($processMethod) {
|
||
|
$processMethod = (string) $processMethod;
|
||
|
if(!ctype_alnum($processMethod)) {
|
||
|
$processMethod = $this->wire()->sanitizer->name($processMethod);
|
||
|
}
|
||
|
$this->processMethodName = $processMethod;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Set the class name prefix used by all related Processes
|
||
|
*
|
||
|
* This is prepended to the class name determined from the URL.
|
||
|
* For example, if the URL indicates a process name is "PageEdit", then we would need a prefix of "Admin"
|
||
|
* to fully resolve the class name.
|
||
|
*
|
||
|
* @param string $prefix
|
||
|
*
|
||
|
*/
|
||
|
public function setPrefix($prefix) {
|
||
|
$prefix = (string) $prefix;
|
||
|
if(!ctype_alpha($prefix)) $prefix = $this->wire()->sanitizer->name($prefix);
|
||
|
$this->prefix = $prefix;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Determine and return the Process to execute
|
||
|
*
|
||
|
* @return Process
|
||
|
*
|
||
|
*/
|
||
|
public function getProcess() {
|
||
|
|
||
|
$modules = $this->wire()->modules;
|
||
|
|
||
|
if($this->process) {
|
||
|
$processName = $this->process->className();
|
||
|
} else if($this->processName) {
|
||
|
$processName = $this->processName;
|
||
|
} else {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
// verify that there is adequate permission to execute the Process
|
||
|
$permissionName = '';
|
||
|
$info = $modules->getModuleInfoVerbose($processName);
|
||
|
$this->processInfo = $info;
|
||
|
if(!empty($info['permission'])) $permissionName = $info['permission'];
|
||
|
|
||
|
$this->hasPermission($permissionName, true); // throws exception if no permission
|
||
|
|
||
|
if(!$this->process) {
|
||
|
$module = $modules->getModule($processName, array('returnError' => true));
|
||
|
if(is_string($module)) {
|
||
|
$this->processError = $module;
|
||
|
$this->process = null;
|
||
|
} else {
|
||
|
$this->process = $module;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// set a process fuel, primarily so that certain Processes can determine if they are the root Process
|
||
|
// example: PageList when in PageEdit
|
||
|
$this->wire('process', $this->process);
|
||
|
|
||
|
return $this->process;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Does the current user have permission to execute the given process name?
|
||
|
*
|
||
|
* Note: an empty permission name is accessible only by the superuser
|
||
|
*
|
||
|
* @param string $permissionName
|
||
|
* @param bool $throw Whether to throw an Exception if the user does not have permission
|
||
|
* @return bool
|
||
|
* @throws ProcessControllerPermissionException
|
||
|
*
|
||
|
*/
|
||
|
protected function hasPermission($permissionName, $throw = true) {
|
||
|
$user = $this->wire()->user;
|
||
|
if($user->isSuperuser()) return true;
|
||
|
if($permissionName && $user->hasPermission($permissionName)) return true;
|
||
|
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) {
|
||
|
|
||
|
$sanitizer = $this->wire()->sanitizer;
|
||
|
$forceFail = false;
|
||
|
$urlSegment1 = $this->wire()->input->urlSegment1;
|
||
|
$method = self::defaultProcessMethodName;
|
||
|
|
||
|
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) {
|
||
|
if($method === 'executed') return '';
|
||
|
if(method_exists($process, $method)) return $method;
|
||
|
if(method_exists($process, "___$method")) return $method;
|
||
|
if($process->hasHook($method . '()')) return $method;
|
||
|
}
|
||
|
|
||
|
// fallback to the unknown, if there is an unknown (you never know)
|
||
|
$method = 'executeUnknown';
|
||
|
if(method_exists($process, $method) || method_exists($process, "___$method")) return $method;
|
||
|
|
||
|
return '';
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Execute the process and return the resulting content generated by the process
|
||
|
*
|
||
|
* @return string
|
||
|
* @throws ProcessController404Exception
|
||
|
*
|
||
|
*/
|
||
|
public function ___execute() {
|
||
|
|
||
|
$debug = $this->wire()->config->debug;
|
||
|
$breadcrumbs = $this->wire()->breadcrumbs;
|
||
|
$headline = $this->wire('processHeadline');
|
||
|
$numBreadcrumbs = $breadcrumbs ? count($breadcrumbs) : null;
|
||
|
$process = $this->getProcess();
|
||
|
|
||
|
if(!$process) {
|
||
|
throw new ProcessController404Exception("Process does not exist: $this->processError");
|
||
|
}
|
||
|
|
||
|
// determine method (throws ProcessControllerPermissionException if no access)
|
||
|
$method = $this->getProcessMethodName($process);
|
||
|
|
||
|
if(!$method) {
|
||
|
throw new ProcessController404Exception("Unrecognized path");
|
||
|
}
|
||
|
|
||
|
if($method === 'executeNavJSON' && !$this->wire()->config->ajax && !$debug) {
|
||
|
// disallow navJSON output when not ajax and not debug mode
|
||
|
if(!$this->wire()->user->isLoggedin()) wire404();
|
||
|
$navJSON = substr($this->wire()->input->url(), -8);
|
||
|
if($navJSON === 'navJSON/') {
|
||
|
$this->wire()->session->location('../');
|
||
|
} else if($navJSON === '/navJSON') {
|
||
|
$this->wire()->session->location('./');
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 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($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 = $process->getViewVars();
|
||
|
}
|
||
|
|
||
|
if(is_array($content)) {
|
||
|
// array of returned content indicates variables to send to a view
|
||
|
if(count($content) || $process->getViewFile()) {
|
||
|
$viewFile = $this->getViewFile($process, $method);
|
||
|
if($viewFile) {
|
||
|
// get output from a separate view file
|
||
|
/** @var TemplateFile $template */
|
||
|
$template = $this->wire(new TemplateFile($viewFile));
|
||
|
foreach($content as $key => $value) {
|
||
|
$template->set($key, $value);
|
||
|
}
|
||
|
$content = $template->render();
|
||
|
}
|
||
|
} else {
|
||
|
$content = '';
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return $content;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Given a process and method name, return the first matching valid view file for it
|
||
|
*
|
||
|
* @param Process $process
|
||
|
* @param string $method If omitted, 'execute' is assumed
|
||
|
* @return string
|
||
|
*
|
||
|
*/
|
||
|
protected function getViewFile(Process $process, $method = '') {
|
||
|
|
||
|
$viewFile = $process->getViewFile();
|
||
|
if($viewFile) return $viewFile;
|
||
|
|
||
|
if(empty($method)) $method = 'execute';
|
||
|
$className = $process->className();
|
||
|
$viewPath = $this->wire()->config->paths->$className;
|
||
|
$method2 = ''; // lowercase hyphenated version
|
||
|
$method3 = ''; // lowercase hyphenated, without leading execute
|
||
|
if(strtolower($method) != $method) {
|
||
|
// lowercase hyphenated version
|
||
|
$method2 = trim(strtolower(preg_replace('/([A-Z]+)/', '-$1', $method)), '-');
|
||
|
// without a leading 'execute-' or 'execute'
|
||
|
$method3 = str_replace(array('execute-', 'execute'), '', $method2);
|
||
|
}
|
||
|
|
||
|
if(is_dir($viewPath . 'views')) {
|
||
|
// check in a /ModuleName/views/ directory for one of the following:
|
||
|
// views/execute.php (only if method name is 'execute')
|
||
|
// views/executeSomeMethod.php
|
||
|
// views/execute-some-method.php
|
||
|
// views/some-method.php (preferable)
|
||
|
$_viewPath = $viewPath;
|
||
|
$viewPath .= 'views/';
|
||
|
$viewFile = $viewPath . $method . '.php'; // i.e. views/execute.php or views/executeSomething.php
|
||
|
if(is_file($viewFile)) return $viewFile;
|
||
|
if($method2) {
|
||
|
// convert executeSomething to execute-something or thisThat to this-that
|
||
|
$viewFile = $viewPath . $method2 . '.php'; // i.e. execute-something.php
|
||
|
if(is_file($viewFile)) return $viewFile;
|
||
|
}
|
||
|
if($method != 'execute' && $method3) {
|
||
|
$viewFile = $viewPath . $method3 . '.php'; // i.e. something.php or some-method.php
|
||
|
if(is_file($viewFile)) return $viewFile;
|
||
|
}
|
||
|
$viewPath = $_viewPath; // restore, since didn't find it in /views/
|
||
|
}
|
||
|
|
||
|
// look for view file in same dir as module
|
||
|
if($method == 'execute') {
|
||
|
$viewFiles = array(
|
||
|
"$className.view.php", // ModuleName.view.php
|
||
|
"$className-execute.view.php", // alt1: ModuleName-execute.view.php
|
||
|
"execute.view.php", // alt2: just execute.view.php (no ModuleName)
|
||
|
);
|
||
|
} else {
|
||
|
$viewFiles = array(
|
||
|
"$className-$method.view.php", // ModuleName.executeSomething.view.php
|
||
|
"$method.view.php", // executeSomething.view.php
|
||
|
);
|
||
|
if($method2) {
|
||
|
$viewFiles[] = "$className-$method2.view.php"; // ModuleName-execute-something.view.php
|
||
|
$viewFiles[] = "$method2.view.php"; // execute-something.view.php
|
||
|
}
|
||
|
if($method3) {
|
||
|
$viewFiles[] = "$className-$method3.view.php"; // ModuleName-something.view.php
|
||
|
$viewFiles[] = "$method3.view.php"; // something.view.php
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// now determine which of the possible view files actually exists
|
||
|
$viewFile = '';
|
||
|
foreach($viewFiles as $file) {
|
||
|
if(is_file($viewPath . $file)) {
|
||
|
$viewFile = $viewPath . $file;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return $viewFile;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Generate a message in JSON format, for use with AJAX output
|
||
|
*
|
||
|
* @param string $msg
|
||
|
* @param bool $error
|
||
|
* @param bool $allowMarkup
|
||
|
* @return string JSON encoded string
|
||
|
*
|
||
|
*/
|
||
|
public function jsonMessage($msg, $error = false, $allowMarkup = false) {
|
||
|
if(!$allowMarkup) $msg = $this->wire()->sanitizer->entities($msg);
|
||
|
return json_encode(array(
|
||
|
'error' => (bool) $error,
|
||
|
'message' => (string) $msg
|
||
|
));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Is this an AJAX request?
|
||
|
*
|
||
|
* @return bool
|
||
|
*
|
||
|
*/
|
||
|
public function isAjax() {
|
||
|
return isset($_SERVER['HTTP_X_REQUESTED_WITH']) && ($_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest');
|
||
|
}
|
||
|
|
||
|
}
|