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