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