translation update
[dokuwiki.git] / inc / ActionRouter.php
blob7d8a72acf8c128b88ea6d796a4cb070ea5873b48
1 <?php
3 namespace dokuwiki;
5 use dokuwiki\Action\AbstractAction;
6 use dokuwiki\Action\Exception\ActionDisabledException;
7 use dokuwiki\Action\Exception\ActionException;
8 use dokuwiki\Action\Exception\FatalException;
9 use dokuwiki\Action\Exception\NoActionException;
10 use dokuwiki\Action\Plugin;
12 /**
13 * Class ActionRouter
14 * @package dokuwiki
16 class ActionRouter {
18 /** @var AbstractAction */
19 protected $action;
21 /** @var ActionRouter */
22 protected static $instance = null;
24 /** @var int transition counter */
25 protected $transitions = 0;
27 /** maximum loop */
28 const MAX_TRANSITIONS = 5;
30 /** @var string[] the actions disabled in the configuration */
31 protected $disabled;
33 /**
34 * ActionRouter constructor. Singleton, thus protected!
36 * Sets up the correct action based on the $ACT global. Writes back
37 * the selected action to $ACT
39 protected function __construct() {
40 global $ACT;
41 global $conf;
43 $this->disabled = explode(',', $conf['disableactions']);
44 $this->disabled = array_map('trim', $this->disabled);
45 $this->transitions = 0;
47 $ACT = act_clean($ACT);
48 $this->setupAction($ACT);
49 $ACT = $this->action->getActionName();
52 /**
53 * Get the singleton instance
55 * @param bool $reinit
56 * @return ActionRouter
58 public static function getInstance($reinit = false) {
59 if((self::$instance === null) || $reinit) {
60 self::$instance = new ActionRouter();
62 return self::$instance;
65 /**
66 * Setup the given action
68 * Instantiates the right class, runs permission checks and pre-processing and
69 * sets $action
71 * @param string $actionname this is passed as a reference to $ACT, for plugin backward compatibility
72 * @triggers ACTION_ACT_PREPROCESS
74 protected function setupAction(&$actionname) {
75 $presetup = $actionname;
77 try {
78 // give plugins an opportunity to process the actionname
79 $evt = new Extension\Event('ACTION_ACT_PREPROCESS', $actionname);
80 if ($evt->advise_before()) {
81 $this->action = $this->loadAction($actionname);
82 $this->checkAction($this->action);
83 $this->action->preProcess();
84 } else {
85 // event said the action should be kept, assume action plugin will handle it later
86 $this->action = new Plugin($actionname);
88 $evt->advise_after();
90 } catch(ActionException $e) {
91 // we should have gotten a new action
92 $actionname = $e->getNewAction();
94 // this one should trigger a user message
95 if(is_a($e, ActionDisabledException::class)) {
96 msg('Action disabled: ' . hsc($presetup), -1);
99 // some actions may request the display of a message
100 if($e->displayToUser()) {
101 msg(hsc($e->getMessage()), -1);
104 // do setup for new action
105 $this->transitionAction($presetup, $actionname);
107 } catch(NoActionException $e) {
108 msg('Action unknown: ' . hsc($actionname), -1);
109 $actionname = 'show';
110 $this->transitionAction($presetup, $actionname);
111 } catch(\Exception $e) {
112 $this->handleFatalException($e);
117 * Transitions from one action to another
119 * Basically just calls setupAction() again but does some checks before.
121 * @param string $from current action name
122 * @param string $to new action name
123 * @param null|ActionException $e any previous exception that caused the transition
125 protected function transitionAction($from, $to, $e = null) {
126 $this->transitions++;
128 // no infinite recursion
129 if($from == $to) {
130 $this->handleFatalException(new FatalException('Infinite loop in actions', 500, $e));
133 // larger loops will be caught here
134 if($this->transitions >= self::MAX_TRANSITIONS) {
135 $this->handleFatalException(new FatalException('Maximum action transitions reached', 500, $e));
138 // do the recursion
139 $this->setupAction($to);
143 * Aborts all processing with a message
145 * When a FataException instanc is passed, the code is treated as Status code
147 * @param \Exception|FatalException $e
148 * @throws FatalException during unit testing
150 protected function handleFatalException(\Exception $e) {
151 if(is_a($e, FatalException::class)) {
152 http_status($e->getCode());
153 } else {
154 http_status(500);
156 if(defined('DOKU_UNITTEST')) {
157 throw $e;
159 $msg = 'Something unforeseen has happened: ' . $e->getMessage();
160 nice_die(hsc($msg));
164 * Load the given action
166 * This translates the given name to a class name by uppercasing the first letter.
167 * Underscores translate to camelcase names. For actions with underscores, the different
168 * parts are removed beginning from the end until a matching class is found. The instatiated
169 * Action will always have the full original action set as Name
171 * Example: 'export_raw' -> ExportRaw then 'export' -> 'Export'
173 * @param $actionname
174 * @return AbstractAction
175 * @throws NoActionException
177 public function loadAction($actionname) {
178 $actionname = strtolower($actionname); // FIXME is this needed here? should we run a cleanup somewhere else?
179 $parts = explode('_', $actionname);
180 while(!empty($parts)) {
181 $load = join('_', $parts);
182 $class = 'dokuwiki\\Action\\' . str_replace('_', '', ucwords($load, '_'));
183 if(class_exists($class)) {
184 return new $class($actionname);
186 array_pop($parts);
189 throw new NoActionException();
193 * Execute all the checks to see if this action can be executed
195 * @param AbstractAction $action
196 * @throws ActionDisabledException
197 * @throws ActionException
199 public function checkAction(AbstractAction $action) {
200 global $INFO;
201 global $ID;
203 if(in_array($action->getActionName(), $this->disabled)) {
204 throw new ActionDisabledException();
207 $action->checkPreconditions();
209 if(isset($INFO)) {
210 $perm = $INFO['perm'];
211 } else {
212 $perm = auth_quickaclcheck($ID);
215 if($perm < $action->minimumPermission()) {
216 throw new ActionException('denied');
221 * Returns the action handling the current request
223 * @return AbstractAction
225 public function getAction() {
226 return $this->action;