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 /** @var AbstractAction */ 20 protected $action; 21 22 /** @var ActionRouter */ 23 protected static $instance; 24 25 /** @var int transition counter */ 26 protected $transitions = 0; 27 28 /** maximum loop */ 29 protected const MAX_TRANSITIONS = 5; 30 31 /** @var string[] the actions disabled in the configuration */ 32 protected $disabled; 33 34 /** 35 * ActionRouter constructor. Singleton, thus protected! 36 * 37 * Sets up the correct action based on the $ACT global. Writes back 38 * the selected action to $ACT 39 */ 40 protected function __construct() 41 { 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 { 61 if ((!self::$instance instanceof \dokuwiki\ActionRouter) || $reinit) { 62 self::$instance = new ActionRouter(); 63 } 64 return self::$instance; 65 } 66 67 /** 68 * Setup the given action 69 * 70 * Instantiates the right class, runs permission checks and pre-processing and 71 * sets $action 72 * 73 * @param string $actionname this is passed as a reference to $ACT, for plugin backward compatibility 74 * @triggers ACTION_ACT_PREPROCESS 75 */ 76 protected function setupAction(&$actionname) 77 { 78 $presetup = $actionname; 79 80 try { 81 // give plugins an opportunity to process the actionname 82 $evt = new Event('ACTION_ACT_PREPROCESS', $actionname); 83 if ($evt->advise_before()) { 84 $this->action = $this->loadAction($actionname); 85 $this->checkAction($this->action); 86 $this->action->preProcess(); 87 } else { 88 // event said the action should be kept, assume action plugin will handle it later 89 $this->action = new Plugin($actionname); 90 } 91 $evt->advise_after(); 92 } catch (ActionException $e) { 93 // we should have gotten a new action 94 $actionname = $e->getNewAction(); 95 96 // this one should trigger a user message 97 if ($e instanceof ActionDisabledException) { 98 msg('Action disabled: ' . hsc($presetup), -1); 99 } 100 101 // some actions may request the display of a message 102 if ($e->displayToUser()) { 103 msg(hsc($e->getMessage()), -1); 104 } 105 106 // do setup for new action 107 $this->transitionAction($presetup, $actionname); 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 { 128 $this->transitions++; 129 130 // no infinite recursion 131 if ($from == $to) { 132 $this->handleFatalException(new FatalException('Infinite loop in actions', 500, $e)); 133 } 134 135 // larger loops will be caught here 136 if ($this->transitions >= self::MAX_TRANSITIONS) { 137 $this->handleFatalException(new FatalException('Maximum action transitions reached', 500, $e)); 138 } 139 140 // do the recursion 141 $this->setupAction($to); 142 } 143 144 /** 145 * Aborts all processing with a message 146 * 147 * When a FataException instanc is passed, the code is treated as Status code 148 * 149 * @param \Exception|FatalException $e 150 * @throws FatalException during unit testing 151 */ 152 protected function handleFatalException(\Throwable $e) 153 { 154 if ($e instanceof FatalException) { 155 http_status($e->getCode()); 156 } else { 157 http_status(500); 158 } 159 if (defined('DOKU_UNITTEST')) { 160 throw $e; 161 } 162 ErrorHandler::logException($e); 163 $msg = 'Something unforeseen has happened: ' . $e->getMessage(); 164 nice_die(hsc($msg)); 165 } 166 167 /** 168 * Load the given action 169 * 170 * This translates the given name to a class name by uppercasing the first letter. 171 * Underscores translate to camelcase names. For actions with underscores, the different 172 * parts are removed beginning from the end until a matching class is found. The instatiated 173 * Action will always have the full original action set as Name 174 * 175 * Example: 'export_raw' -> ExportRaw then 'export' -> 'Export' 176 * 177 * @param $actionname 178 * @return AbstractAction 179 * @throws NoActionException 180 */ 181 public function loadAction($actionname) 182 { 183 $actionname = strtolower($actionname); // FIXME is this needed here? should we run a cleanup somewhere else? 184 $parts = explode('_', $actionname); 185 while ($parts !== []) { 186 $load = implode('_', $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 { 206 global $INFO; 207 global $ID; 208 209 if (in_array($action->getActionName(), $this->disabled)) { 210 throw new ActionDisabledException(); 211 } 212 213 $action->checkPreconditions(); 214 215 if (isset($INFO)) { 216 $perm = $INFO['perm']; 217 } else { 218 $perm = auth_quickaclcheck($ID); 219 } 220 221 if ($perm < $action->minimumPermission()) { 222 throw new ActionException('denied'); 223 } 224 } 225 226 /** 227 * Returns the action handling the current request 228 * 229 * @return AbstractAction 230 */ 231 public function getAction() 232 { 233 return $this->action; 234 } 235} 236