164ab5140SAndreas Gohr<?php 264ab5140SAndreas Gohr 364ab5140SAndreas Gohrnamespace dokuwiki; 464ab5140SAndreas Gohr 564ab5140SAndreas Gohruse dokuwiki\Action\AbstractAction; 664ab5140SAndreas Gohruse dokuwiki\Action\Exception\ActionDisabledException; 764ab5140SAndreas Gohruse dokuwiki\Action\Exception\ActionException; 864ab5140SAndreas Gohruse dokuwiki\Action\Exception\FatalException; 964ab5140SAndreas Gohruse dokuwiki\Action\Exception\NoActionException; 10a3f6fae6SAndreas Gohruse dokuwiki\Action\Plugin; 1164ab5140SAndreas Gohr 12a3f6fae6SAndreas Gohr/** 13a3f6fae6SAndreas Gohr * Class ActionRouter 14a3f6fae6SAndreas Gohr * @package dokuwiki 15a3f6fae6SAndreas Gohr */ 1664ab5140SAndreas Gohrclass ActionRouter { 1764ab5140SAndreas Gohr 1864ab5140SAndreas Gohr /** @var AbstractAction */ 1964ab5140SAndreas Gohr protected $action; 2064ab5140SAndreas Gohr 2164ab5140SAndreas Gohr /** @var ActionRouter */ 22ae7bcdc7SAndreas Gohr protected static $instance; 2364ab5140SAndreas Gohr 2450701b66SAndreas Gohr /** @var int transition counter */ 2550701b66SAndreas Gohr protected $transitions = 0; 2650701b66SAndreas Gohr 2750701b66SAndreas Gohr /** maximum loop */ 2850701b66SAndreas Gohr const MAX_TRANSITIONS = 5; 2950701b66SAndreas Gohr 30480336a3SAndreas Gohr /** @var string[] the actions disabled in the configuration */ 31480336a3SAndreas Gohr protected $disabled; 32480336a3SAndreas Gohr 3364ab5140SAndreas Gohr /** 3464ab5140SAndreas Gohr * ActionRouter constructor. Singleton, thus protected! 35a3f6fae6SAndreas Gohr * 36a3f6fae6SAndreas Gohr * Sets up the correct action based on the $ACT global. Writes back 37a3f6fae6SAndreas Gohr * the selected action to $ACT 3864ab5140SAndreas Gohr */ 3964ab5140SAndreas Gohr protected function __construct() { 40a3f6fae6SAndreas Gohr global $ACT; 41480336a3SAndreas Gohr global $conf; 42480336a3SAndreas Gohr 43480336a3SAndreas Gohr $this->disabled = explode(',', $conf['disableactions']); 44480336a3SAndreas Gohr $this->disabled = array_map('trim', $this->disabled); 45480336a3SAndreas Gohr $this->transitions = 0; 46480336a3SAndreas Gohr 47a3f6fae6SAndreas Gohr $ACT = act_clean($ACT); 48a3f6fae6SAndreas Gohr $this->setupAction($ACT); 49a3f6fae6SAndreas Gohr $ACT = $this->action->getActionName(); 5064ab5140SAndreas Gohr } 5164ab5140SAndreas Gohr 5264ab5140SAndreas Gohr /** 5364ab5140SAndreas Gohr * Get the singleton instance 5464ab5140SAndreas Gohr * 5564ab5140SAndreas Gohr * @param bool $reinit 5664ab5140SAndreas Gohr * @return ActionRouter 5764ab5140SAndreas Gohr */ 58ae7bcdc7SAndreas Gohr public static function getInstance($reinit = false) { 59ae7bcdc7SAndreas Gohr if((self::$instance === null) || $reinit) { 60ae7bcdc7SAndreas Gohr self::$instance = new ActionRouter(); 6164ab5140SAndreas Gohr } 62ae7bcdc7SAndreas Gohr return self::$instance; 6364ab5140SAndreas Gohr } 6464ab5140SAndreas Gohr 6564ab5140SAndreas Gohr /** 6664ab5140SAndreas Gohr * Setup the given action 6764ab5140SAndreas Gohr * 6864ab5140SAndreas Gohr * Instantiates the right class, runs permission checks and pre-processing and 69a3f6fae6SAndreas Gohr * sets $action 7064ab5140SAndreas Gohr * 7164ab5140SAndreas Gohr * @param string $actionname 72a3f6fae6SAndreas Gohr * @triggers ACTION_ACT_PREPROCESS 7364ab5140SAndreas Gohr */ 7464ab5140SAndreas Gohr protected function setupAction($actionname) { 75a3f6fae6SAndreas Gohr $presetup = $actionname; 76a3f6fae6SAndreas Gohr 7764ab5140SAndreas Gohr try { 7864ab5140SAndreas Gohr $this->action = $this->loadAction($actionname); 79480336a3SAndreas Gohr $this->checkAction($this->action); 8064ab5140SAndreas Gohr $this->action->preProcess(); 8164ab5140SAndreas Gohr 8264ab5140SAndreas Gohr } catch(ActionException $e) { 8364ab5140SAndreas Gohr // we should have gotten a new action 84a3f6fae6SAndreas Gohr $actionname = $e->getNewAction(); 8564ab5140SAndreas Gohr 8664ab5140SAndreas Gohr // this one should trigger a user message 8764ab5140SAndreas Gohr if(is_a($e, ActionDisabledException::class)) { 88a3f6fae6SAndreas Gohr msg('Action disabled: ' . hsc($presetup), -1); 8964ab5140SAndreas Gohr } 9064ab5140SAndreas Gohr 91*81f9e22bSAndreas Gohr // some actions may request the display of a message 92*81f9e22bSAndreas Gohr if($e->displayToUser()) { 93*81f9e22bSAndreas Gohr msg(hsc($e->getMessage()), -1); 94*81f9e22bSAndreas Gohr } 95*81f9e22bSAndreas Gohr 9664ab5140SAndreas Gohr // do setup for new action 9750701b66SAndreas Gohr $this->transitionAction($presetup, $actionname); 9864ab5140SAndreas Gohr 9964ab5140SAndreas Gohr } catch(NoActionException $e) { 100a3f6fae6SAndreas Gohr // give plugins an opportunity to process the actionname 101a3f6fae6SAndreas Gohr $evt = new \Doku_Event('ACTION_ACT_PREPROCESS', $actionname); 102a3f6fae6SAndreas Gohr if($evt->advise_before()) { 103a3f6fae6SAndreas Gohr if($actionname == $presetup) { 104a3f6fae6SAndreas Gohr // no plugin changed the action, complain and switch to show 105a3f6fae6SAndreas Gohr msg('Action unknown: ' . hsc($actionname), -1); 106a3f6fae6SAndreas Gohr $actionname = 'show'; 107a3f6fae6SAndreas Gohr } 10850701b66SAndreas Gohr $this->transitionAction($presetup, $actionname); 109a3f6fae6SAndreas Gohr } else { 110a3f6fae6SAndreas Gohr // event said the action should be kept, assume action plugin will handle it later 11173522543SAndreas Gohr $this->action = new Plugin($actionname); 112a3f6fae6SAndreas Gohr } 113a3f6fae6SAndreas Gohr $evt->advise_after(); 11464ab5140SAndreas Gohr 11564ab5140SAndreas Gohr } catch(\Exception $e) { 11664ab5140SAndreas Gohr $this->handleFatalException($e); 11764ab5140SAndreas Gohr } 11864ab5140SAndreas Gohr } 11964ab5140SAndreas Gohr 12064ab5140SAndreas Gohr /** 12150701b66SAndreas Gohr * Transitions from one action to another 12250701b66SAndreas Gohr * 12350701b66SAndreas Gohr * Basically just calls setupAction() again but does some checks before. Also triggers 12450701b66SAndreas Gohr * redirects for POST to show transitions 12550701b66SAndreas Gohr * 12650701b66SAndreas Gohr * @param string $from current action name 12750701b66SAndreas Gohr * @param string $to new action name 12850701b66SAndreas Gohr * @param null|ActionException $e any previous exception that caused the transition 12950701b66SAndreas Gohr */ 13050701b66SAndreas Gohr protected function transitionAction($from, $to, $e = null) { 13150701b66SAndreas Gohr global $INPUT; 13250701b66SAndreas Gohr global $ID; 13350701b66SAndreas Gohr 13450701b66SAndreas Gohr $this->transitions++; 13550701b66SAndreas Gohr 13650701b66SAndreas Gohr // no infinite recursion 13750701b66SAndreas Gohr if($from == $to) { 13850701b66SAndreas Gohr $this->handleFatalException(new FatalException('Infinite loop in actions', 500, $e)); 13950701b66SAndreas Gohr } 14050701b66SAndreas Gohr 14150701b66SAndreas Gohr // larger loops will be caught here 14250701b66SAndreas Gohr if($this->transitions >= self::MAX_TRANSITIONS) { 14350701b66SAndreas Gohr $this->handleFatalException(new FatalException('Maximum action transitions reached', 500, $e)); 14450701b66SAndreas Gohr } 14550701b66SAndreas Gohr 14650701b66SAndreas Gohr // POST transitions to show should be a redirect 14716d428e9SAndreas Gohr if($to == 'show' && strtolower($INPUT->server->str('REQUEST_METHOD')) == 'post') { 14850701b66SAndreas Gohr act_redirect($ID, $from); // FIXME we may want to move this function to the class 14950701b66SAndreas Gohr } 15050701b66SAndreas Gohr 15150701b66SAndreas Gohr // do the recursion 15250701b66SAndreas Gohr $this->setupAction($to); 15350701b66SAndreas Gohr } 15450701b66SAndreas Gohr 15550701b66SAndreas Gohr /** 15664ab5140SAndreas Gohr * Aborts all processing with a message 15764ab5140SAndreas Gohr * 15864ab5140SAndreas Gohr * When a FataException instanc is passed, the code is treated as Status code 15964ab5140SAndreas Gohr * 16064ab5140SAndreas Gohr * @param \Exception|FatalException $e 16164ab5140SAndreas Gohr */ 16264ab5140SAndreas Gohr protected function handleFatalException(\Exception $e) { 16364ab5140SAndreas Gohr if(is_a($e, FatalException::class)) { 16464ab5140SAndreas Gohr http_status($e->getCode()); 16564ab5140SAndreas Gohr } else { 16664ab5140SAndreas Gohr http_status(500); 16764ab5140SAndreas Gohr } 16864ab5140SAndreas Gohr $msg = 'Something unforseen has happened: ' . $e->getMessage(); 16964ab5140SAndreas Gohr nice_die(hsc($msg)); 17064ab5140SAndreas Gohr } 17164ab5140SAndreas Gohr 17264ab5140SAndreas Gohr /** 17364ab5140SAndreas Gohr * Load the given action 17464ab5140SAndreas Gohr * 17573522543SAndreas Gohr * This translates the given name to a class name by uppercasing the first letter. 17673522543SAndreas Gohr * Underscores translate to camelcase names. For actions with underscores, the different 17773522543SAndreas Gohr * parts are removed beginning from the end until a matching class is found. The instatiated 17873522543SAndreas Gohr * Action will always have the full original action set as Name 17973522543SAndreas Gohr * 18073522543SAndreas Gohr * Example: 'export_raw' -> ExportRaw then 'export' -> 'Export' 18173522543SAndreas Gohr * 18264ab5140SAndreas Gohr * @param $actionname 18364ab5140SAndreas Gohr * @return AbstractAction 18464ab5140SAndreas Gohr * @throws NoActionException 18564ab5140SAndreas Gohr */ 186480336a3SAndreas Gohr public function loadAction($actionname) { 18773522543SAndreas Gohr $actionname = strtolower($actionname); // FIXME is this needed here? should we run a cleanup somewhere else? 18873522543SAndreas Gohr $parts = explode('_', $actionname); 18973522543SAndreas Gohr while($parts) { 19073522543SAndreas Gohr $load = join('_', $parts); 19173522543SAndreas Gohr $class = 'dokuwiki\\Action\\' . str_replace('_', '', ucwords($load, '_')); 19264ab5140SAndreas Gohr if(class_exists($class)) { 19373522543SAndreas Gohr return new $class($actionname); 19464ab5140SAndreas Gohr } 19573522543SAndreas Gohr array_pop($parts); 19673522543SAndreas Gohr } 19773522543SAndreas Gohr 19864ab5140SAndreas Gohr throw new NoActionException(); 19964ab5140SAndreas Gohr } 20064ab5140SAndreas Gohr 20164ab5140SAndreas Gohr /** 202480336a3SAndreas Gohr * Execute all the checks to see if this action can be executed 203480336a3SAndreas Gohr * 204480336a3SAndreas Gohr * @param AbstractAction $action 205480336a3SAndreas Gohr * @throws ActionDisabledException 206480336a3SAndreas Gohr * @throws ActionException 207480336a3SAndreas Gohr */ 208480336a3SAndreas Gohr public function checkAction(AbstractAction $action) { 209480336a3SAndreas Gohr global $INFO; 210480336a3SAndreas Gohr global $ID; 211480336a3SAndreas Gohr 212480336a3SAndreas Gohr if(in_array($action->getActionName(), $this->disabled)) { 213480336a3SAndreas Gohr throw new ActionDisabledException(); 214480336a3SAndreas Gohr } 215480336a3SAndreas Gohr 216480336a3SAndreas Gohr $action->checkPermissions(); 217480336a3SAndreas Gohr 218480336a3SAndreas Gohr if(isset($INFO)) { 219480336a3SAndreas Gohr $perm = $INFO['perm']; 220480336a3SAndreas Gohr } else { 221480336a3SAndreas Gohr $perm = auth_quickaclcheck($ID); 222480336a3SAndreas Gohr } 223480336a3SAndreas Gohr 224480336a3SAndreas Gohr if($perm < $action->minimumPermission()) { 225480336a3SAndreas Gohr throw new ActionException('denied'); 226480336a3SAndreas Gohr } 227480336a3SAndreas Gohr } 228480336a3SAndreas Gohr 229480336a3SAndreas Gohr /** 23064ab5140SAndreas Gohr * Returns the action handling the current request 23164ab5140SAndreas Gohr * 23264ab5140SAndreas Gohr * @return AbstractAction 23364ab5140SAndreas Gohr */ 23464ab5140SAndreas Gohr public function getAction() { 23564ab5140SAndreas Gohr return $this->action; 23664ab5140SAndreas Gohr } 23764ab5140SAndreas Gohr} 238