1<?php 2 3namespace dokuwiki; 4 5use dokuwiki\Action\AbstractAction; 6use dokuwiki\Action\Exception\ActionDisabledException; 7use dokuwiki\Action\Exception\ActionException; 8use dokuwiki\Action\Exception\FatalException; 9use dokuwiki\Action\Exception\NoActionException; 10use dokuwiki\Action\Plugin; 11 12/** 13 * Class ActionRouter 14 * @package dokuwiki 15 */ 16class ActionRouter { 17 18 /** @var AbstractAction */ 19 protected $action; 20 21 /** @var ActionRouter */ 22 protected static $instance = NULL; 23 24 /** @var int transition counter */ 25 protected $transitions = 0; 26 27 /** maximum loop */ 28 const MAX_TRANSITIONS = 5; 29 30 /** @var string[] the actions disabled in the configuration */ 31 protected $disabled; 32 33 /** 34 * ActionRouter constructor. Singleton, thus protected! 35 * 36 * Sets up the correct action based on the $ACT global. Writes back 37 * the selected action to $ACT 38 */ 39 protected function __construct() { 40 global $ACT; 41 global $conf; 42 43 $this->disabled = explode(',', $conf['disableactions']); 44 $this->disabled = array_map('trim', $this->disabled); 45 $this->transitions = 0; 46 47 if(defined('DOKU_UNITTEST') && (self::$instance !== null)) { 48 $ACT = act_clean($ACT); 49 $this->setupAction($ACT); 50 } else { 51 $ACT = act_clean($ACT); 52 $this->setupAction($ACT); 53 $ACT = $this->action->getActionName(); 54 } 55 } 56 57 /** 58 * Get the singleton instance 59 * 60 * @param bool $reinit 61 * @return ActionRouter 62 */ 63 public static function getInstance($reinit = false) { 64 if((self::$instance === null) || $reinit) { 65 self::$instance = new ActionRouter(); 66 } 67 return self::$instance; 68 } 69 70 /** 71 * Setup the given action 72 * 73 * Instantiates the right class, runs permission checks and pre-processing and 74 * sets $action 75 * 76 * @param string $actionname 77 * @triggers ACTION_ACT_PREPROCESS 78 */ 79 protected function setupAction($actionname) { 80 $presetup = $actionname; 81 82 try { 83 $this->action = $this->loadAction($actionname); 84 $this->checkAction($this->action); 85 $this->action->preProcess(); 86 87 } catch(ActionException $e) { 88 // we should have gotten a new action 89 $actionname = $e->getNewAction(); 90 91 // this one should trigger a user message 92 if(is_a($e, ActionDisabledException::class)) { 93 msg('Action disabled: ' . hsc($presetup), -1); 94 } 95 96 // some actions may request the display of a message 97 if($e->displayToUser()) { 98 msg(hsc($e->getMessage()), -1); 99 } 100 101 // do setup for new action 102 $this->transitionAction($presetup, $actionname); 103 104 } catch(NoActionException $e) { 105 // give plugins an opportunity to process the actionname 106 $evt = new \Doku_Event('ACTION_ACT_PREPROCESS', $actionname); 107 if($evt->advise_before()) { 108 if($actionname == $presetup) { 109 // no plugin changed the action, complain and switch to show 110 msg('Action unknown: ' . hsc($actionname), -1); 111 $actionname = 'show'; 112 } 113 $this->transitionAction($presetup, $actionname); 114 } else { 115 // event said the action should be kept, assume action plugin will handle it later 116 $this->action = new Plugin($actionname); 117 } 118 $evt->advise_after(); 119 120 } catch(\Exception $e) { 121 $this->handleFatalException($e); 122 } 123 } 124 125 /** 126 * Transitions from one action to another 127 * 128 * Basically just calls setupAction() again but does some checks before. 129 * 130 * @param string $from current action name 131 * @param string $to new action name 132 * @param null|ActionException $e any previous exception that caused the transition 133 */ 134 protected function transitionAction($from, $to, $e = null) { 135 $this->transitions++; 136 137 // no infinite recursion 138 if($from == $to) { 139 $this->handleFatalException(new FatalException('Infinite loop in actions', 500, $e)); 140 } 141 142 // larger loops will be caught here 143 if($this->transitions >= self::MAX_TRANSITIONS) { 144 $this->handleFatalException(new FatalException('Maximum action transitions reached', 500, $e)); 145 } 146 147 // do the recursion 148 $this->setupAction($to); 149 } 150 151 /** 152 * Aborts all processing with a message 153 * 154 * When a FataException instanc is passed, the code is treated as Status code 155 * 156 * @param \Exception|FatalException $e 157 */ 158 protected function handleFatalException(\Exception $e) { 159 if(is_a($e, FatalException::class)) { 160 http_status($e->getCode()); 161 } else { 162 http_status(500); 163 } 164 $msg = 'Something unforseen has happened: ' . $e->getMessage(); 165 nice_die(hsc($msg)); 166 } 167 168 /** 169 * Load the given action 170 * 171 * This translates the given name to a class name by uppercasing the first letter. 172 * Underscores translate to camelcase names. For actions with underscores, the different 173 * parts are removed beginning from the end until a matching class is found. The instatiated 174 * Action will always have the full original action set as Name 175 * 176 * Example: 'export_raw' -> ExportRaw then 'export' -> 'Export' 177 * 178 * @param $actionname 179 * @return AbstractAction 180 * @throws NoActionException 181 */ 182 public function loadAction($actionname) { 183 $actionname = strtolower($actionname); // FIXME is this needed here? should we run a cleanup somewhere else? 184 $parts = explode('_', $actionname); 185 while(!empty($parts)) { 186 $load = join('_', $parts); 187 $class = 'dokuwiki\\Action\\' . str_replace('_', '', ucwords($load, '_')); 188 if(class_exists($class)) { 189 return new $class($actionname); 190 } 191 array_pop($parts); 192 } 193 194 throw new NoActionException(); 195 } 196 197 /** 198 * Execute all the checks to see if this action can be executed 199 * 200 * @param AbstractAction $action 201 * @throws ActionDisabledException 202 * @throws ActionException 203 */ 204 public function checkAction(AbstractAction $action) { 205 global $INFO; 206 global $ID; 207 208 if(in_array($action->getActionName(), $this->disabled)) { 209 throw new ActionDisabledException(); 210 } 211 212 $action->checkPermissions(); 213 214 if(isset($INFO)) { 215 $perm = $INFO['perm']; 216 } else { 217 $perm = auth_quickaclcheck($ID); 218 } 219 220 if($perm < $action->minimumPermission()) { 221 throw new ActionException('denied'); 222 } 223 } 224 225 /** 226 * Returns the action handling the current request 227 * 228 * @return AbstractAction 229 */ 230 public function getAction() { 231 return $this->action; 232 } 233} 234