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