5 use dokuwiki\Extension\Event
;
6 use dokuwiki\Action\AbstractAction
;
7 use dokuwiki\Action\Exception\ActionDisabledException
;
8 use dokuwiki\Action\Exception\ActionException
;
9 use dokuwiki\Action\Exception\FatalException
;
10 use dokuwiki\Action\Exception\NoActionException
;
11 use dokuwiki\Action\Plugin
;
20 /** @var AbstractAction */
23 /** @var ActionRouter */
24 protected static $instance;
26 /** @var int transition counter */
27 protected $transitions = 0;
30 protected const MAX_TRANSITIONS
= 5;
32 /** @var string[] the actions disabled in the configuration */
36 * ActionRouter constructor. Singleton, thus protected!
38 * Sets up the correct action based on the $ACT global. Writes back
39 * the selected action to $ACT
41 protected function __construct()
46 $this->disabled
= explode(',', $conf['disableactions']);
47 $this->disabled
= array_map('trim', $this->disabled
);
49 $ACT = act_clean($ACT);
50 $this->setupAction($ACT);
51 $ACT = $this->action
->getActionName();
55 * Get the singleton instance
58 * @return ActionRouter
60 public static function getInstance($reinit = false)
62 if ((!self
::$instance instanceof \dokuwiki\ActionRouter
) ||
$reinit) {
63 self
::$instance = new ActionRouter();
65 return self
::$instance;
69 * Setup the given action
71 * Instantiates the right class, runs permission checks and pre-processing and
74 * @param string $actionname this is passed as a reference to $ACT, for plugin backward compatibility
75 * @triggers ACTION_ACT_PREPROCESS
77 protected function setupAction(&$actionname)
79 $presetup = $actionname;
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();
89 // event said the action should be kept, assume action plugin will handle it later
90 $this->action
= new Plugin($actionname);
93 } catch (ActionException
$e) {
94 // we should have gotten a new action
95 $actionname = $e->getNewAction();
97 // this one should trigger a user message
98 if ($e instanceof ActionDisabledException
) {
99 msg('Action disabled: ' . hsc($presetup), -1);
102 // some actions may request the display of a message
103 if ($e->displayToUser()) {
104 msg(hsc($e->getMessage()), -1);
107 // do setup for new action
108 $this->transitionAction($presetup, $actionname);
109 } catch (NoActionException
$e) {
110 msg('Action unknown: ' . hsc($actionname), -1);
111 $actionname = 'show';
112 $this->transitionAction($presetup, $actionname);
113 } catch (\Exception
$e) {
114 $this->handleFatalException($e);
119 * Transitions from one action to another
121 * Basically just calls setupAction() again but does some checks before.
123 * @param string $from current action name
124 * @param string $to new action name
125 * @param null|ActionException $e any previous exception that caused the transition
127 protected function transitionAction($from, $to, $e = null)
129 $this->transitions++
;
131 // no infinite recursion
133 $this->handleFatalException(new FatalException('Infinite loop in actions', 500, $e));
136 // larger loops will be caught here
137 if ($this->transitions
>= self
::MAX_TRANSITIONS
) {
138 $this->handleFatalException(new FatalException('Maximum action transitions reached', 500, $e));
142 $this->setupAction($to);
146 * Aborts all processing with a message
148 * When a FataException instanc is passed, the code is treated as Status code
150 * @param \Exception|FatalException $e
151 * @throws FatalException during unit testing
153 protected function handleFatalException(\Throwable
$e)
155 if ($e instanceof FatalException
) {
156 http_status($e->getCode());
160 if (defined('DOKU_UNITTEST')) {
163 ErrorHandler
::logException($e);
164 $msg = 'Something unforeseen has happened: ' . $e->getMessage();
169 * Load the given action
171 * This translates the given name to a class name by uppercasing the first letter.
172 * Underscores translate to camelcase names. For actions with underscores, the different
173 * parts are removed beginning from the end until a matching class is found. The instatiated
174 * Action will always have the full original action set as Name
176 * Example: 'export_raw' -> ExportRaw then 'export' -> 'Export'
179 * @return AbstractAction
180 * @throws NoActionException
182 public function loadAction($actionname)
184 $actionname = strtolower($actionname); // FIXME is this needed here? should we run a cleanup somewhere else?
185 $parts = explode('_', $actionname);
186 while ($parts !== []) {
187 $load = implode('_', $parts);
188 $class = 'dokuwiki\\Action\\' . str_replace('_', '', ucwords($load, '_'));
189 if (class_exists($class)) {
190 return new $class($actionname);
195 throw new NoActionException();
199 * Execute all the checks to see if this action can be executed
201 * @param AbstractAction $action
202 * @throws ActionDisabledException
203 * @throws ActionException
205 public function checkAction(AbstractAction
$action)
210 if (in_array($action->getActionName(), $this->disabled
)) {
211 throw new ActionDisabledException();
214 $action->checkPreconditions();
217 $perm = $INFO['perm'];
219 $perm = auth_quickaclcheck($ID);
222 if ($perm < $action->minimumPermission()) {
223 throw new ActionException('denied');
228 * Returns the action handling the current request
230 * @return AbstractAction
232 public function getAction()
234 return $this->action
;