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 /** 31 * ActionRouter constructor. Singleton, thus protected! 32 * 33 * Sets up the correct action based on the $ACT global. Writes back 34 * the selected action to $ACT 35 */ 36 protected function __construct() { 37 global $ACT; 38 $ACT = act_clean($ACT); 39 $this->setupAction($ACT); 40 $ACT = $this->action->getActionName(); 41 } 42 43 /** 44 * Get the singleton instance 45 * 46 * @param bool $reinit 47 * @return ActionRouter 48 */ 49 public static function getInstance($reinit = false) { 50 if((self::$instance === null) || $reinit) { 51 self::$instance = new ActionRouter(); 52 } 53 return self::$instance; 54 } 55 56 /** 57 * Setup the given action 58 * 59 * Instantiates the right class, runs permission checks and pre-processing and 60 * sets $action 61 * 62 * @param string $actionname 63 * @triggers ACTION_ACT_PREPROCESS 64 */ 65 protected function setupAction($actionname) { 66 $presetup = $actionname; 67 68 try { 69 $this->action = $this->loadAction($actionname); 70 $this->action->checkPermissions(); 71 $this->ensureMinimumPermission($this->action->minimumPermission()); 72 $this->action->preProcess(); 73 74 } catch(ActionException $e) { 75 // we should have gotten a new action 76 $actionname = $e->getNewAction(); 77 78 // this one should trigger a user message 79 if(is_a($e, ActionDisabledException::class)) { 80 msg('Action disabled: ' . hsc($presetup), -1); 81 } 82 83 // do setup for new action 84 $this->transitionAction($presetup, $actionname); 85 86 } catch(NoActionException $e) { 87 // give plugins an opportunity to process the actionname 88 $evt = new \Doku_Event('ACTION_ACT_PREPROCESS', $actionname); 89 if($evt->advise_before()) { 90 if($actionname == $presetup) { 91 // no plugin changed the action, complain and switch to show 92 msg('Action unknown: ' . hsc($actionname), -1); 93 $actionname = 'show'; 94 } 95 $this->transitionAction($presetup, $actionname); 96 } else { 97 // event said the action should be kept, assume action plugin will handle it later 98 $this->action = new Plugin($actionname); 99 } 100 $evt->advise_after(); 101 102 } catch(\Exception $e) { 103 $this->handleFatalException($e); 104 } 105 } 106 107 /** 108 * Transitions from one action to another 109 * 110 * Basically just calls setupAction() again but does some checks before. Also triggers 111 * redirects for POST to show transitions 112 * 113 * @param string $from current action name 114 * @param string $to new action name 115 * @param null|ActionException $e any previous exception that caused the transition 116 */ 117 protected function transitionAction($from, $to, $e = null) { 118 global $INPUT; 119 global $ID; 120 121 $this->transitions++; 122 123 // no infinite recursion 124 if($from == $to) { 125 $this->handleFatalException(new FatalException('Infinite loop in actions', 500, $e)); 126 } 127 128 // larger loops will be caught here 129 if($this->transitions >= self::MAX_TRANSITIONS) { 130 $this->handleFatalException(new FatalException('Maximum action transitions reached', 500, $e)); 131 } 132 133 // POST transitions to show should be a redirect 134 if($to == 'show' && $from != $to && strtolower($INPUT->server->str('REQUEST_METHOD')) == 'post') { 135 act_redirect($ID, $from); // FIXME we may want to move this function to the class 136 } 137 138 // do the recursion 139 $this->setupAction($to); 140 } 141 142 /** 143 * Check that the given minimum permissions are reached 144 * 145 * @param int $permneed 146 * @throws ActionException 147 */ 148 protected function ensureMinimumPermission($permneed) { 149 global $INFO; 150 if($INFO['perm'] < $permneed) { 151 throw new ActionException('denied'); 152 } 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 protected 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 * Returns the action handling the current request 203 * 204 * @return AbstractAction 205 */ 206 public function getAction() { 207 return $this->action; 208 } 209} 210