xref: /plugin/captcha/action.php (revision 09b1e97e3cb9f2c4be8ca729baa9d49a3ba58ba1)
142a27035SAndreas Gohr<?php
218622736SAndreas Gohr
3*09b1e97eSAndreas Gohruse dokuwiki\Extension\ActionPlugin;
4*09b1e97eSAndreas Gohruse dokuwiki\Extension\EventHandler;
5*09b1e97eSAndreas Gohruse dokuwiki\Extension\Event;
6*09b1e97eSAndreas Gohruse dokuwiki\Form\Form;
7969b14c4SAndreas Gohruse dokuwiki\plugin\captcha\IpCounter;
8969b14c4SAndreas Gohr
942a27035SAndreas Gohr/**
1042a27035SAndreas Gohr * CAPTCHA antispam plugin
1142a27035SAndreas Gohr *
1242a27035SAndreas Gohr * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
1342a27035SAndreas Gohr * @author     Andreas Gohr <gohr@cosmocode.de>
1442a27035SAndreas Gohr */
15*09b1e97eSAndreas Gohrclass action_plugin_captcha extends ActionPlugin
161c08a51cSAndreas Gohr{
1742a27035SAndreas Gohr    /**
1842a27035SAndreas Gohr     * register the eventhandlers
1942a27035SAndreas Gohr     */
20*09b1e97eSAndreas Gohr    public function register(EventHandler $controller)
211c08a51cSAndreas Gohr    {
227218f96cSAndreas Gohr        // check CAPTCHA success
231c08a51cSAndreas Gohr        $controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'handle_captcha_input', []);
2442a27035SAndreas Gohr
257218f96cSAndreas Gohr        // inject in edit form
261c08a51cSAndreas Gohr        $controller->register_hook('HTML_EDITFORM_OUTPUT', 'BEFORE', $this, 'handle_form_output', []); //old
271c08a51cSAndreas Gohr        $controller->register_hook('FORM_EDIT_OUTPUT', 'BEFORE', $this, 'handle_form_output', []); //new
2842a27035SAndreas Gohr
297218f96cSAndreas Gohr        // inject in user registration
301c08a51cSAndreas Gohr        $controller->register_hook('HTML_REGISTERFORM_OUTPUT', 'BEFORE', $this, 'handle_form_output', []); //old
311c08a51cSAndreas Gohr        $controller->register_hook('FORM_REGISTER_OUTPUT', 'BEFORE', $this, 'handle_form_output', []); //new
32f74276b8SAndreas Gohr
33f74276b8SAndreas Gohr        // inject in password reset
341c08a51cSAndreas Gohr        $controller->register_hook('HTML_RESENDPWDFORM_OUTPUT', 'BEFORE', $this, 'handle_form_output', []); //old
351c08a51cSAndreas Gohr        $controller->register_hook('FORM_RESENDPWD_OUTPUT', 'BEFORE', $this, 'handle_form_output', []); //new
36643f15bdSAndreas Gohr
37643f15bdSAndreas Gohr        // inject in login form
381c08a51cSAndreas Gohr        $controller->register_hook('HTML_LOGINFORM_OUTPUT', 'BEFORE', $this, 'handle_form_output', []); // old
391c08a51cSAndreas Gohr        $controller->register_hook('FORM_LOGIN_OUTPUT', 'BEFORE', $this, 'handle_form_output', []); // new
401c08a51cSAndreas Gohr
41643f15bdSAndreas Gohr        // check on login
421c08a51cSAndreas Gohr        $controller->register_hook('AUTH_LOGIN_CHECK', 'BEFORE', $this, 'handle_login', []);
43cde3ece1SAndreas Gohr
44cde3ece1SAndreas Gohr        // clean up captcha cookies
451c08a51cSAndreas Gohr        $controller->register_hook('INDEXER_TASKS_RUN', 'AFTER', $this, 'handle_indexer', []);
46969b14c4SAndreas Gohr
47*09b1e97eSAndreas Gohr        $this->getConf('loginprotect');
48969b14c4SAndreas Gohr
49969b14c4SAndreas Gohr        // log authentication failures
50969b14c4SAndreas Gohr        if ((int)$this->getConf('loginprotect') > 1) {
51969b14c4SAndreas Gohr            $controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'handle_auth', []);
52969b14c4SAndreas Gohr        }
5342a27035SAndreas Gohr    }
5442a27035SAndreas Gohr
5542a27035SAndreas Gohr    /**
56bd26d35bSAndreas Gohr     * Check if the current mode should be handled by CAPTCHA
57bd26d35bSAndreas Gohr     *
58f74276b8SAndreas Gohr     * Note: checking needs to be done when a form has been submitted, not when the form
59f74276b8SAndreas Gohr     * is shown for the first time. Except for the editing process this is not determined
60f74276b8SAndreas Gohr     * by $act alone but needs to inspect other input variables.
61f74276b8SAndreas Gohr     *
62bd26d35bSAndreas Gohr     * @param string $act cleaned action mode
63bd26d35bSAndreas Gohr     * @return bool
64bd26d35bSAndreas Gohr     */
651c08a51cSAndreas Gohr    protected function needs_checking($act)
661c08a51cSAndreas Gohr    {
67bd26d35bSAndreas Gohr        global $INPUT;
68bd26d35bSAndreas Gohr
69bd26d35bSAndreas Gohr        switch ($act) {
70bd26d35bSAndreas Gohr            case 'save':
71bd26d35bSAndreas Gohr                return true;
72bd26d35bSAndreas Gohr            case 'register':
73f74276b8SAndreas Gohr            case 'resendpwd':
74bd26d35bSAndreas Gohr                return $INPUT->bool('save');
75643f15bdSAndreas Gohr            case 'login':
76643f15bdSAndreas Gohr                // we do not handle this here, but in handle_login()
77bd26d35bSAndreas Gohr            default:
78bd26d35bSAndreas Gohr                return false;
79bd26d35bSAndreas Gohr        }
80bd26d35bSAndreas Gohr    }
81bd26d35bSAndreas Gohr
82bd26d35bSAndreas Gohr    /**
83bd26d35bSAndreas Gohr     * Aborts the given mode
84bd26d35bSAndreas Gohr     *
85bd26d35bSAndreas Gohr     * Aborting depends on the mode. It might unset certain input parameters or simply switch
86bd26d35bSAndreas Gohr     * the mode to something else (giving as return which needs to be passed back to the
87bd26d35bSAndreas Gohr     * ACTION_ACT_PREPROCESS event)
88bd26d35bSAndreas Gohr     *
89bd26d35bSAndreas Gohr     * @param string $act cleaned action mode
90bd26d35bSAndreas Gohr     * @return string the new mode to use
91bd26d35bSAndreas Gohr     */
921c08a51cSAndreas Gohr    protected function abort_action($act)
931c08a51cSAndreas Gohr    {
94bd26d35bSAndreas Gohr        global $INPUT;
95bd26d35bSAndreas Gohr
96bd26d35bSAndreas Gohr        switch ($act) {
97bd26d35bSAndreas Gohr            case 'save':
98bd26d35bSAndreas Gohr                return 'preview';
99bd26d35bSAndreas Gohr            case 'register':
100f74276b8SAndreas Gohr            case 'resendpwd':
101bd26d35bSAndreas Gohr                $INPUT->post->set('save', false);
102f74276b8SAndreas Gohr                return $act;
103643f15bdSAndreas Gohr            case 'login':
104643f15bdSAndreas Gohr                // we do not handle this here, but in handle_login()
105bd26d35bSAndreas Gohr            default:
106bd26d35bSAndreas Gohr                return $act;
107bd26d35bSAndreas Gohr        }
108bd26d35bSAndreas Gohr    }
109bd26d35bSAndreas Gohr
110bd26d35bSAndreas Gohr    /**
111969b14c4SAndreas Gohr     * Should a login CAPTCHA be used?
112969b14c4SAndreas Gohr     *
113969b14c4SAndreas Gohr     * @return bool
114969b14c4SAndreas Gohr     */
115969b14c4SAndreas Gohr    protected function protectLogin()
116969b14c4SAndreas Gohr    {
117969b14c4SAndreas Gohr        $config = (int)$this->getConf('loginprotect');
118969b14c4SAndreas Gohr        if ($config < 1) return false; // not wanted
119969b14c4SAndreas Gohr        if ($config === 1) return true; // always wanted
120969b14c4SAndreas Gohr        $count = (new IpCounter())->get();
121969b14c4SAndreas Gohr        return $count > 2; // only after 3 failed attempts
122969b14c4SAndreas Gohr    }
123969b14c4SAndreas Gohr
124969b14c4SAndreas Gohr    /**
125643f15bdSAndreas Gohr     * Handles CAPTCHA check in login
126643f15bdSAndreas Gohr     *
127643f15bdSAndreas Gohr     * Logins happen very early in the DokuWiki lifecycle, so we have to intercept them
128643f15bdSAndreas Gohr     * in their own event.
129643f15bdSAndreas Gohr     *
130*09b1e97eSAndreas Gohr     * @param Event $event
131643f15bdSAndreas Gohr     * @param $param
132643f15bdSAndreas Gohr     */
133*09b1e97eSAndreas Gohr    public function handle_login(Event $event, $param)
1341c08a51cSAndreas Gohr    {
135643f15bdSAndreas Gohr        global $INPUT;
136969b14c4SAndreas Gohr        if (!$this->protectLogin()) return; // no protection wanted
137643f15bdSAndreas Gohr        if (!$INPUT->bool('u')) return; // this login was not triggered by a form
138643f15bdSAndreas Gohr
139643f15bdSAndreas Gohr        // we need to have $ID set for the captcha check
140643f15bdSAndreas Gohr        global $ID;
141643f15bdSAndreas Gohr        $ID = getID();
142643f15bdSAndreas Gohr
143643f15bdSAndreas Gohr        /** @var helper_plugin_captcha $helper */
144643f15bdSAndreas Gohr        $helper = plugin_load('helper', 'captcha');
145643f15bdSAndreas Gohr        if (!$helper->check()) {
146643f15bdSAndreas Gohr            $event->data['silent'] = true; // we have our own message
147643f15bdSAndreas Gohr            $event->result = false; // login fail
148643f15bdSAndreas Gohr            $event->preventDefault();
149643f15bdSAndreas Gohr            $event->stopPropagation();
150643f15bdSAndreas Gohr        }
151643f15bdSAndreas Gohr    }
152643f15bdSAndreas Gohr
153643f15bdSAndreas Gohr    /**
154643f15bdSAndreas Gohr     * Intercept all actions and check for CAPTCHA first.
15542a27035SAndreas Gohr     */
156*09b1e97eSAndreas Gohr    public function handle_captcha_input(Event $event, $param)
1571c08a51cSAndreas Gohr    {
15864382f29SAndreas Gohr        global $INPUT;
15964382f29SAndreas Gohr
1607218f96cSAndreas Gohr        $act = act_clean($event->data);
161f74276b8SAndreas Gohr        if (!$this->needs_checking($act)) return;
16293f66506SAndreas Gohr
16342a27035SAndreas Gohr        // do nothing if logged in user and no CAPTCHA required
16464382f29SAndreas Gohr        if (!$this->getConf('forusers') && $INPUT->server->str('REMOTE_USER')) {
16542a27035SAndreas Gohr            return;
16642a27035SAndreas Gohr        }
16742a27035SAndreas Gohr
16877e00bf9SAndreas Gohr        // check captcha
1697218f96cSAndreas Gohr        /** @var helper_plugin_captcha $helper */
17077e00bf9SAndreas Gohr        $helper = plugin_load('helper', 'captcha');
17177e00bf9SAndreas Gohr        if (!$helper->check()) {
172bd26d35bSAndreas Gohr            $event->data = $this->abort_action($act);
17342a27035SAndreas Gohr        }
17442a27035SAndreas Gohr    }
17542a27035SAndreas Gohr
17642a27035SAndreas Gohr    /**
1771c08a51cSAndreas Gohr     * Inject the CAPTCHA in a DokuForm or \dokuwiki\Form\Form
17842a27035SAndreas Gohr     */
179*09b1e97eSAndreas Gohr    public function handle_form_output(Event $event, $param)
1801c08a51cSAndreas Gohr    {
18164382f29SAndreas Gohr        global $INPUT;
18264382f29SAndreas Gohr
183c0439b03SAndreas Gohr        if (
184c0439b03SAndreas Gohr            ($event->name === 'FORM_LOGIN_OUTPUT' || $event->name === 'HTML_LOGINFORM_OUTPUT')
185c0439b03SAndreas Gohr            &&
186c0439b03SAndreas Gohr            !$this->protectLogin()
187c0439b03SAndreas Gohr        ) {
188c0439b03SAndreas Gohr            // no login protection wanted
189c0439b03SAndreas Gohr            return;
190c0439b03SAndreas Gohr        }
19131c8e2bdSAndreas Gohr
192*09b1e97eSAndreas Gohr        /** @var Form|\Doku_Form $form */
1931c08a51cSAndreas Gohr        $form = $event->data;
1941c08a51cSAndreas Gohr
19547afabe6SAndreas Gohr        // get position of submit button
196*09b1e97eSAndreas Gohr        if (is_a($form, Form::class)) {
1971c08a51cSAndreas Gohr            $pos = $form->findPositionByAttribute('type', 'submit');
1981c08a51cSAndreas Gohr        } else {
1991c08a51cSAndreas Gohr            $pos = $form->findElementByAttribute('type', 'submit');
2001c08a51cSAndreas Gohr        }
20147afabe6SAndreas Gohr        if (!$pos) return; // no button -> source view mode
20247afabe6SAndreas Gohr
20342a27035SAndreas Gohr        // do nothing if logged in user and no CAPTCHA required
20464382f29SAndreas Gohr        if (!$this->getConf('forusers') && $INPUT->server->str('REMOTE_USER')) {
20542a27035SAndreas Gohr            return;
20642a27035SAndreas Gohr        }
20742a27035SAndreas Gohr
20877e00bf9SAndreas Gohr        // get the CAPTCHA
2097218f96cSAndreas Gohr        /** @var helper_plugin_captcha $helper */
21077e00bf9SAndreas Gohr        $helper = plugin_load('helper', 'captcha');
21177e00bf9SAndreas Gohr        $out = $helper->getHTML();
21247afabe6SAndreas Gohr
2131c08a51cSAndreas Gohr        // insert before the submit button
214*09b1e97eSAndreas Gohr        if (is_a($form, Form::class)) {
2151c08a51cSAndreas Gohr            $form->addHTML($out, $pos);
2161c08a51cSAndreas Gohr        } else {
2171c08a51cSAndreas Gohr            $form->insertElement($pos, $out);
2181c08a51cSAndreas Gohr        }
21942a27035SAndreas Gohr    }
22042a27035SAndreas Gohr
221cde3ece1SAndreas Gohr    /**
222cde3ece1SAndreas Gohr     * Clean cookies once per day
223cde3ece1SAndreas Gohr     */
224*09b1e97eSAndreas Gohr    public function handle_indexer(Event $event, $param)
2251c08a51cSAndreas Gohr    {
226cde3ece1SAndreas Gohr        $lastrun = getCacheName('captcha', '.captcha');
227cde3ece1SAndreas Gohr        $last = @filemtime($lastrun);
228cde3ece1SAndreas Gohr        if (time() - $last < 24 * 60 * 60) return;
229cde3ece1SAndreas Gohr
230cde3ece1SAndreas Gohr        /** @var helper_plugin_captcha $helper */
231cde3ece1SAndreas Gohr        $helper = plugin_load('helper', 'captcha');
232*09b1e97eSAndreas Gohr        $helper->cleanCaptchaCookies();
2335d59bd09SAndreas Gohr        touch($lastrun);
234cde3ece1SAndreas Gohr
235cde3ece1SAndreas Gohr        $event->preventDefault();
236cde3ece1SAndreas Gohr        $event->stopPropagation();
237cde3ece1SAndreas Gohr    }
238969b14c4SAndreas Gohr
239969b14c4SAndreas Gohr    /**
240969b14c4SAndreas Gohr     * Count failed login attempts
241969b14c4SAndreas Gohr     */
242*09b1e97eSAndreas Gohr    public function handle_auth(Event $event, $param)
243969b14c4SAndreas Gohr    {
244969b14c4SAndreas Gohr        global $INPUT;
245969b14c4SAndreas Gohr        $act = act_clean($event->data);
246969b14c4SAndreas Gohr        if (
247969b14c4SAndreas Gohr            $act != 'logout' &&
24831c8e2bdSAndreas Gohr            $INPUT->str('u') !== '' &&
249969b14c4SAndreas Gohr            empty($INPUT->server->str('http_credentials')) &&
250969b14c4SAndreas Gohr            empty($INPUT->server->str('REMOTE_USER'))
251969b14c4SAndreas Gohr        ) {
252969b14c4SAndreas Gohr            // This is a failed authentication attempt, count it
253969b14c4SAndreas Gohr            (new IpCounter())->increment();
254969b14c4SAndreas Gohr        }
255969b14c4SAndreas Gohr
256969b14c4SAndreas Gohr        if (
257969b14c4SAndreas Gohr            $act == 'login' &&
258969b14c4SAndreas Gohr            !empty($INPUT->server->str('REMOTE_USER'))
259969b14c4SAndreas Gohr        ) {
260969b14c4SAndreas Gohr            // This is a successful login, reset the counter
261969b14c4SAndreas Gohr            (new IpCounter())->reset();
262969b14c4SAndreas Gohr        }
263969b14c4SAndreas Gohr    }
26442a27035SAndreas Gohr}
265