about summary refs log tree commit diff stats
path: root/wiki/inc/ActionRouter.php
blob: edc45cfc40ca6e815b86f6b344c1cfa82bf91e5b (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
<?php

namespace dokuwiki;

use dokuwiki\Action\AbstractAction;
use dokuwiki\Action\Exception\ActionDisabledException;
use dokuwiki\Action\Exception\ActionException;
use dokuwiki\Action\Exception\FatalException;
use dokuwiki\Action\Exception\NoActionException;
use dokuwiki\Action\Plugin;

/**
 * Class ActionRouter
 * @package dokuwiki
 */
class ActionRouter {

    /** @var  AbstractAction */
    protected $action;

    /** @var  ActionRouter */
    protected static $instance = null;

    /** @var int transition counter */
    protected $transitions = 0;

    /** maximum loop */
    const MAX_TRANSITIONS = 5;

    /** @var string[] the actions disabled in the configuration */
    protected $disabled;

    /**
     * ActionRouter constructor. Singleton, thus protected!
     *
     * Sets up the correct action based on the $ACT global. Writes back
     * the selected action to $ACT
     */
    protected function __construct() {
        global $ACT;
        global $conf;

        $this->disabled = explode(',', $conf['disableactions']);
        $this->disabled = array_map('trim', $this->disabled);
        $this->transitions = 0;

        $ACT = act_clean($ACT);
        $this->setupAction($ACT);
        $ACT = $this->action->getActionName();
    }

    /**
     * Get the singleton instance
     *
     * @param bool $reinit
     * @return ActionRouter
     */
    public static function getInstance($reinit = false) {
        if((self::$instance === null) || $reinit) {
            self::$instance = new ActionRouter();
        }
        return self::$instance;
    }

    /**
     * Setup the given action
     *
     * Instantiates the right class, runs permission checks and pre-processing and
     * sets $action
     *
     * @param string $actionname this is passed as a reference to $ACT, for plugin backward compatibility
     * @triggers ACTION_ACT_PREPROCESS
     */
    protected function setupAction(&$actionname) {
        $presetup = $actionname;

        try {
            // give plugins an opportunity to process the actionname
            $evt = new \Doku_Event('ACTION_ACT_PREPROCESS', $actionname);
            if ($evt->advise_before()) {
                $this->action = $this->loadAction($actionname);
                $this->checkAction($this->action);
                $this->action->preProcess();
            } else {
                // event said the action should be kept, assume action plugin will handle it later
                $this->action = new Plugin($actionname);
            }
            $evt->advise_after();

        } catch(ActionException $e) {
            // we should have gotten a new action
            $actionname = $e->getNewAction();

            // this one should trigger a user message
            if(is_a($e, ActionDisabledException::class)) {
                msg('Action disabled: ' . hsc($presetup), -1);
            }

            // some actions may request the display of a message
            if($e->displayToUser()) {
                msg(hsc($e->getMessage()), -1);
            }

            // do setup for new action
            $this->transitionAction($presetup, $actionname);

        } catch(NoActionException $e) {
            msg('Action unknown: ' . hsc($actionname), -1);
            $actionname = 'show';
            $this->transitionAction($presetup, $actionname);
        } catch(\Exception $e) {
            $this->handleFatalException($e);
        }
    }

    /**
     * Transitions from one action to another
     *
     * Basically just calls setupAction() again but does some checks before.
     *
     * @param string $from current action name
     * @param string $to new action name
     * @param null|ActionException $e any previous exception that caused the transition
     */
    protected function transitionAction($from, $to, $e = null) {
        $this->transitions++;

        // no infinite recursion
        if($from == $to) {
            $this->handleFatalException(new FatalException('Infinite loop in actions', 500, $e));
        }

        // larger loops will be caught here
        if($this->transitions >= self::MAX_TRANSITIONS) {
            $this->handleFatalException(new FatalException('Maximum action transitions reached', 500, $e));
        }

        // do the recursion
        $this->setupAction($to);
    }

    /**
     * Aborts all processing with a message
     *
     * When a FataException instanc is passed, the code is treated as Status code
     *
     * @param \Exception|FatalException $e
     * @throws FatalException during unit testing
     */
    protected function handleFatalException(\Exception $e) {
        if(is_a($e, FatalException::class)) {
            http_status($e->getCode());
        } else {
            http_status(500);
        }
        if(defined('DOKU_UNITTEST')) {
            throw $e;
        }
        $msg = 'Something unforseen has happened: ' . $e->getMessage();
        nice_die(hsc($msg));
    }

    /**
     * Load the given action
     *
     * This translates the given name to a class name by uppercasing the first letter.
     * Underscores translate to camelcase names. For actions with underscores, the different
     * parts are removed beginning from the end until a matching class is found. The instatiated
     * Action will always have the full original action set as Name
     *
     * Example: 'export_raw' -> ExportRaw then 'export' -> 'Export'
     *
     * @param $actionname
     * @return AbstractAction
     * @throws NoActionException
     */
    public function loadAction($actionname) {
        $actionname = strtolower($actionname); // FIXME is this needed here? should we run a cleanup somewhere else?
        $parts = explode('_', $actionname);
        while(!empty($parts)) {
            $load = join('_', $parts);
            $class = 'dokuwiki\\Action\\' . str_replace('_', '', ucwords($load, '_'));
            if(class_exists($class)) {
                return new $class($actionname);
            }
            array_pop($parts);
        }

        throw new NoActionException();
    }

    /**
     * Execute all the checks to see if this action can be executed
     *
     * @param AbstractAction $action
     * @throws ActionDisabledException
     * @throws ActionException
     */
    public function checkAction(AbstractAction $action) {
        global $INFO;
        global $ID;

        if(in_array($action->getActionName(), $this->disabled)) {
            throw new ActionDisabledException();
        }

        $action->checkPreconditions();

        if(isset($INFO)) {
            $perm = $INFO['perm'];
        } else {
            $perm = auth_quickaclcheck($ID);
        }

        if($perm < $action->minimumPermission()) {
            throw new ActionException('denied');
        }
    }

    /**
     * Returns the action handling the current request
     *
     * @return AbstractAction
     */
    public function getAction() {
        return $this->action;
    }
}