Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | Download
<?php namespace ProcessWire;/*** ProcessWire ProcessController** Loads and executes Process Module instance and determines access.** ProcessWire 3.x, Copyright 2018 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() {$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) {$this->processName = $this->sanitizer->name($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) {$this->processMethodName = $this->sanitizer->name($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) {$this->prefix = $this->sanitizer->name($prefix);}/*** Determine and return the Process to execute** @return Process**/public function getProcess() {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 = $this->wire('modules')->getModuleInfoVerbose($processName);$this->processInfo = $info;if(!empty($info['permission'])) $permissionName = $info['permission'];$this->hasPermission($permissionName, true); // throws exception if no permissionif(!$this->process) {$module = $this->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) {$forceFail = false;$urlSegment1 = $this->wire('input')->urlSegment1;$method = self::defaultProcessMethodName;$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) {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");}// 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() methodif($method !== 'execute') {// some method other than the main oneif(!is_null($numBreadcrumbs) && $numBreadcrumbs === count($breadcrumbs)) {// process added no breadcrumbs, but there should be moreif($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 viewif(count($content) || $process->getViewFile()) {$viewFile = $this->getViewFile($process, $method);if($viewFile) {// get output from a separate view file$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 executeif(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.phpif(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.phpif(is_file($viewFile)) return $viewFile;}if($method != 'execute' && $method3) {$viewFile = $viewPath . $method3 . '.php'; // i.e. something.php or some-method.phpif(is_file($viewFile)) return $viewFile;}$viewPath = $_viewPath; // restore, since didn't find it in /views/}// look for view file in same dir as moduleif($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* @return string JSON encoded string**/public function jsonMessage($msg, $error = false) {return json_encode(array('error' => $error,'message' => $msg));}/*** Is this an AJAX request?** @return bool**/public function isAjax() {return isset($_SERVER['HTTP_X_REQUESTED_WITH']) && ($_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest');}}