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