xref: /dokuwiki/inc/ActionRouter.php (revision ae7bcdc71a9ea54b34e8a4a206e2c6d4ff8623d8)
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();
99                $this->action->setActionName($actionname);
100            }
101            $evt->advise_after();
102
103        } catch(\Exception $e) {
104            $this->handleFatalException($e);
105        }
106    }
107
108    /**
109     * Transitions from one action to another
110     *
111     * Basically just calls setupAction() again but does some checks before. Also triggers
112     * redirects for POST to show transitions
113     *
114     * @param string $from current action name
115     * @param string $to new action name
116     * @param null|ActionException $e any previous exception that caused the transition
117     */
118    protected function transitionAction($from, $to, $e = null) {
119        global $INPUT;
120        global $ID;
121
122        $this->transitions++;
123
124        // no infinite recursion
125        if($from == $to) {
126            $this->handleFatalException(new FatalException('Infinite loop in actions', 500, $e));
127        }
128
129        // larger loops will be caught here
130        if($this->transitions >= self::MAX_TRANSITIONS) {
131            $this->handleFatalException(new FatalException('Maximum action transitions reached', 500, $e));
132        }
133
134        // POST transitions to show should be a redirect
135        if($to == 'show' && $from != $to && strtolower($INPUT->server->str('REQUEST_METHOD')) == 'post') {
136            act_redirect($ID, $from); // FIXME we may want to move this function to the class
137        }
138
139        // do the recursion
140        $this->setupAction($to);
141    }
142
143    /**
144     * Check that the given minimum permissions are reached
145     *
146     * @param int $permneed
147     * @throws ActionException
148     */
149    protected function ensureMinimumPermission($permneed) {
150        global $INFO;
151        if($INFO['perm'] < $permneed) {
152            throw new ActionException('denied');
153        }
154    }
155
156    /**
157     * Aborts all processing with a message
158     *
159     * When a FataException instanc is passed, the code is treated as Status code
160     *
161     * @param \Exception|FatalException $e
162     */
163    protected function handleFatalException(\Exception $e) {
164        if(is_a($e, FatalException::class)) {
165            http_status($e->getCode());
166        } else {
167            http_status(500);
168        }
169        $msg = 'Something unforseen has happened: ' . $e->getMessage();
170        nice_die(hsc($msg));
171    }
172
173    /**
174     * Load the given action
175     *
176     * @param $actionname
177     * @return AbstractAction
178     * @throws NoActionException
179     */
180    protected function loadAction($actionname) {
181        $class = 'dokuwiki\\Action\\' . ucfirst(strtolower($actionname));
182        if(class_exists($class)) {
183            return new $class;
184        }
185        throw new NoActionException();
186    }
187
188    /**
189     * Returns the action handling the current request
190     *
191     * @return AbstractAction
192     */
193    public function getAction() {
194        return $this->action;
195    }
196}
197