xref: /plugin/captcha/action.php (revision 09b1e97e3cb9f2c4be8ca729baa9d49a3ba58ba1)
1<?php
2
3use dokuwiki\Extension\ActionPlugin;
4use dokuwiki\Extension\EventHandler;
5use dokuwiki\Extension\Event;
6use dokuwiki\Form\Form;
7use dokuwiki\plugin\captcha\IpCounter;
8
9/**
10 * CAPTCHA antispam plugin
11 *
12 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
13 * @author     Andreas Gohr <gohr@cosmocode.de>
14 */
15class action_plugin_captcha extends ActionPlugin
16{
17    /**
18     * register the eventhandlers
19     */
20    public function register(EventHandler $controller)
21    {
22        // check CAPTCHA success
23        $controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'handle_captcha_input', []);
24
25        // inject in edit form
26        $controller->register_hook('HTML_EDITFORM_OUTPUT', 'BEFORE', $this, 'handle_form_output', []); //old
27        $controller->register_hook('FORM_EDIT_OUTPUT', 'BEFORE', $this, 'handle_form_output', []); //new
28
29        // inject in user registration
30        $controller->register_hook('HTML_REGISTERFORM_OUTPUT', 'BEFORE', $this, 'handle_form_output', []); //old
31        $controller->register_hook('FORM_REGISTER_OUTPUT', 'BEFORE', $this, 'handle_form_output', []); //new
32
33        // inject in password reset
34        $controller->register_hook('HTML_RESENDPWDFORM_OUTPUT', 'BEFORE', $this, 'handle_form_output', []); //old
35        $controller->register_hook('FORM_RESENDPWD_OUTPUT', 'BEFORE', $this, 'handle_form_output', []); //new
36
37        // inject in login form
38        $controller->register_hook('HTML_LOGINFORM_OUTPUT', 'BEFORE', $this, 'handle_form_output', []); // old
39        $controller->register_hook('FORM_LOGIN_OUTPUT', 'BEFORE', $this, 'handle_form_output', []); // new
40
41        // check on login
42        $controller->register_hook('AUTH_LOGIN_CHECK', 'BEFORE', $this, 'handle_login', []);
43
44        // clean up captcha cookies
45        $controller->register_hook('INDEXER_TASKS_RUN', 'AFTER', $this, 'handle_indexer', []);
46
47        $this->getConf('loginprotect');
48
49        // log authentication failures
50        if ((int)$this->getConf('loginprotect') > 1) {
51            $controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'handle_auth', []);
52        }
53    }
54
55    /**
56     * Check if the current mode should be handled by CAPTCHA
57     *
58     * Note: checking needs to be done when a form has been submitted, not when the form
59     * is shown for the first time. Except for the editing process this is not determined
60     * by $act alone but needs to inspect other input variables.
61     *
62     * @param string $act cleaned action mode
63     * @return bool
64     */
65    protected function needs_checking($act)
66    {
67        global $INPUT;
68
69        switch ($act) {
70            case 'save':
71                return true;
72            case 'register':
73            case 'resendpwd':
74                return $INPUT->bool('save');
75            case 'login':
76                // we do not handle this here, but in handle_login()
77            default:
78                return false;
79        }
80    }
81
82    /**
83     * Aborts the given mode
84     *
85     * Aborting depends on the mode. It might unset certain input parameters or simply switch
86     * the mode to something else (giving as return which needs to be passed back to the
87     * ACTION_ACT_PREPROCESS event)
88     *
89     * @param string $act cleaned action mode
90     * @return string the new mode to use
91     */
92    protected function abort_action($act)
93    {
94        global $INPUT;
95
96        switch ($act) {
97            case 'save':
98                return 'preview';
99            case 'register':
100            case 'resendpwd':
101                $INPUT->post->set('save', false);
102                return $act;
103            case 'login':
104                // we do not handle this here, but in handle_login()
105            default:
106                return $act;
107        }
108    }
109
110    /**
111     * Should a login CAPTCHA be used?
112     *
113     * @return bool
114     */
115    protected function protectLogin()
116    {
117        $config = (int)$this->getConf('loginprotect');
118        if ($config < 1) return false; // not wanted
119        if ($config === 1) return true; // always wanted
120        $count = (new IpCounter())->get();
121        return $count > 2; // only after 3 failed attempts
122    }
123
124    /**
125     * Handles CAPTCHA check in login
126     *
127     * Logins happen very early in the DokuWiki lifecycle, so we have to intercept them
128     * in their own event.
129     *
130     * @param Event $event
131     * @param $param
132     */
133    public function handle_login(Event $event, $param)
134    {
135        global $INPUT;
136        if (!$this->protectLogin()) return; // no protection wanted
137        if (!$INPUT->bool('u')) return; // this login was not triggered by a form
138
139        // we need to have $ID set for the captcha check
140        global $ID;
141        $ID = getID();
142
143        /** @var helper_plugin_captcha $helper */
144        $helper = plugin_load('helper', 'captcha');
145        if (!$helper->check()) {
146            $event->data['silent'] = true; // we have our own message
147            $event->result = false; // login fail
148            $event->preventDefault();
149            $event->stopPropagation();
150        }
151    }
152
153    /**
154     * Intercept all actions and check for CAPTCHA first.
155     */
156    public function handle_captcha_input(Event $event, $param)
157    {
158        global $INPUT;
159
160        $act = act_clean($event->data);
161        if (!$this->needs_checking($act)) return;
162
163        // do nothing if logged in user and no CAPTCHA required
164        if (!$this->getConf('forusers') && $INPUT->server->str('REMOTE_USER')) {
165            return;
166        }
167
168        // check captcha
169        /** @var helper_plugin_captcha $helper */
170        $helper = plugin_load('helper', 'captcha');
171        if (!$helper->check()) {
172            $event->data = $this->abort_action($act);
173        }
174    }
175
176    /**
177     * Inject the CAPTCHA in a DokuForm or \dokuwiki\Form\Form
178     */
179    public function handle_form_output(Event $event, $param)
180    {
181        global $INPUT;
182
183        if (
184            ($event->name === 'FORM_LOGIN_OUTPUT' || $event->name === 'HTML_LOGINFORM_OUTPUT')
185            &&
186            !$this->protectLogin()
187        ) {
188            // no login protection wanted
189            return;
190        }
191
192        /** @var Form|\Doku_Form $form */
193        $form = $event->data;
194
195        // get position of submit button
196        if (is_a($form, Form::class)) {
197            $pos = $form->findPositionByAttribute('type', 'submit');
198        } else {
199            $pos = $form->findElementByAttribute('type', 'submit');
200        }
201        if (!$pos) return; // no button -> source view mode
202
203        // do nothing if logged in user and no CAPTCHA required
204        if (!$this->getConf('forusers') && $INPUT->server->str('REMOTE_USER')) {
205            return;
206        }
207
208        // get the CAPTCHA
209        /** @var helper_plugin_captcha $helper */
210        $helper = plugin_load('helper', 'captcha');
211        $out = $helper->getHTML();
212
213        // insert before the submit button
214        if (is_a($form, Form::class)) {
215            $form->addHTML($out, $pos);
216        } else {
217            $form->insertElement($pos, $out);
218        }
219    }
220
221    /**
222     * Clean cookies once per day
223     */
224    public function handle_indexer(Event $event, $param)
225    {
226        $lastrun = getCacheName('captcha', '.captcha');
227        $last = @filemtime($lastrun);
228        if (time() - $last < 24 * 60 * 60) return;
229
230        /** @var helper_plugin_captcha $helper */
231        $helper = plugin_load('helper', 'captcha');
232        $helper->cleanCaptchaCookies();
233        touch($lastrun);
234
235        $event->preventDefault();
236        $event->stopPropagation();
237    }
238
239    /**
240     * Count failed login attempts
241     */
242    public function handle_auth(Event $event, $param)
243    {
244        global $INPUT;
245        $act = act_clean($event->data);
246        if (
247            $act != 'logout' &&
248            $INPUT->str('u') !== '' &&
249            empty($INPUT->server->str('http_credentials')) &&
250            empty($INPUT->server->str('REMOTE_USER'))
251        ) {
252            // This is a failed authentication attempt, count it
253            (new IpCounter())->increment();
254        }
255
256        if (
257            $act == 'login' &&
258            !empty($INPUT->server->str('REMOTE_USER'))
259        ) {
260            // This is a successful login, reset the counter
261            (new IpCounter())->reset();
262        }
263    }
264}
265