xref: /plugin/captcha/action.php (revision 64382f292cb5c1c7154e8fd095ef7180282ebf84)
142a27035SAndreas Gohr<?php
218622736SAndreas Gohr
3969b14c4SAndreas Gohruse dokuwiki\plugin\captcha\IpCounter;
4969b14c4SAndreas Gohr
542a27035SAndreas Gohr/**
642a27035SAndreas Gohr * CAPTCHA antispam plugin
742a27035SAndreas Gohr *
842a27035SAndreas Gohr * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
942a27035SAndreas Gohr * @author     Andreas Gohr <gohr@cosmocode.de>
1042a27035SAndreas Gohr */
111c08a51cSAndreas Gohrclass action_plugin_captcha extends DokuWiki_Action_Plugin
121c08a51cSAndreas Gohr{
1342a27035SAndreas Gohr
1442a27035SAndreas Gohr    /**
1542a27035SAndreas Gohr     * register the eventhandlers
1642a27035SAndreas Gohr     */
171c08a51cSAndreas Gohr    public function register(Doku_Event_Handler $controller)
181c08a51cSAndreas Gohr    {
197218f96cSAndreas Gohr        // check CAPTCHA success
201c08a51cSAndreas Gohr        $controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'handle_captcha_input', []);
2142a27035SAndreas Gohr
227218f96cSAndreas Gohr        // inject in edit form
231c08a51cSAndreas Gohr        $controller->register_hook('HTML_EDITFORM_OUTPUT', 'BEFORE', $this, 'handle_form_output', []); //old
241c08a51cSAndreas Gohr        $controller->register_hook('FORM_EDIT_OUTPUT', 'BEFORE', $this, 'handle_form_output', []); //new
2542a27035SAndreas Gohr
267218f96cSAndreas Gohr        // inject in user registration
271c08a51cSAndreas Gohr        $controller->register_hook('HTML_REGISTERFORM_OUTPUT', 'BEFORE', $this, 'handle_form_output', []); //old
281c08a51cSAndreas Gohr        $controller->register_hook('FORM_REGISTER_OUTPUT', 'BEFORE', $this, 'handle_form_output', []); //new
29f74276b8SAndreas Gohr
30f74276b8SAndreas Gohr        // inject in password reset
311c08a51cSAndreas Gohr        $controller->register_hook('HTML_RESENDPWDFORM_OUTPUT', 'BEFORE', $this, 'handle_form_output', []); //old
321c08a51cSAndreas Gohr        $controller->register_hook('FORM_RESENDPWD_OUTPUT', 'BEFORE', $this, 'handle_form_output', []); //new
33643f15bdSAndreas Gohr
34643f15bdSAndreas Gohr        // inject in login form
351c08a51cSAndreas Gohr        $controller->register_hook('HTML_LOGINFORM_OUTPUT', 'BEFORE', $this, 'handle_form_output', []); // old
361c08a51cSAndreas Gohr        $controller->register_hook('FORM_LOGIN_OUTPUT', 'BEFORE', $this, 'handle_form_output', []); // new
371c08a51cSAndreas Gohr
38643f15bdSAndreas Gohr        // check on login
391c08a51cSAndreas Gohr        $controller->register_hook('AUTH_LOGIN_CHECK', 'BEFORE', $this, 'handle_login', []);
40cde3ece1SAndreas Gohr
41cde3ece1SAndreas Gohr        // clean up captcha cookies
421c08a51cSAndreas Gohr        $controller->register_hook('INDEXER_TASKS_RUN', 'AFTER', $this, 'handle_indexer', []);
43969b14c4SAndreas Gohr
44969b14c4SAndreas Gohr        $onk = $this->getConf('loginprotect');
45969b14c4SAndreas Gohr
46969b14c4SAndreas Gohr        // log authentication failures
47969b14c4SAndreas Gohr        if ((int)$this->getConf('loginprotect') > 1) {
48969b14c4SAndreas Gohr            $controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'handle_auth', []);
49969b14c4SAndreas Gohr        }
5042a27035SAndreas Gohr    }
5142a27035SAndreas Gohr
5242a27035SAndreas Gohr    /**
53bd26d35bSAndreas Gohr     * Check if the current mode should be handled by CAPTCHA
54bd26d35bSAndreas Gohr     *
55f74276b8SAndreas Gohr     * Note: checking needs to be done when a form has been submitted, not when the form
56f74276b8SAndreas Gohr     * is shown for the first time. Except for the editing process this is not determined
57f74276b8SAndreas Gohr     * by $act alone but needs to inspect other input variables.
58f74276b8SAndreas Gohr     *
59bd26d35bSAndreas Gohr     * @param string $act cleaned action mode
60bd26d35bSAndreas Gohr     * @return bool
61bd26d35bSAndreas Gohr     */
621c08a51cSAndreas Gohr    protected function needs_checking($act)
631c08a51cSAndreas Gohr    {
64bd26d35bSAndreas Gohr        global $INPUT;
65bd26d35bSAndreas Gohr
66bd26d35bSAndreas Gohr        switch ($act) {
67bd26d35bSAndreas Gohr            case 'save':
68bd26d35bSAndreas Gohr                return true;
69bd26d35bSAndreas Gohr            case 'register':
70f74276b8SAndreas Gohr            case 'resendpwd':
71bd26d35bSAndreas Gohr                return $INPUT->bool('save');
72643f15bdSAndreas Gohr            case 'login':
73643f15bdSAndreas Gohr                // we do not handle this here, but in handle_login()
74bd26d35bSAndreas Gohr            default:
75bd26d35bSAndreas Gohr                return false;
76bd26d35bSAndreas Gohr        }
77bd26d35bSAndreas Gohr    }
78bd26d35bSAndreas Gohr
79bd26d35bSAndreas Gohr    /**
80bd26d35bSAndreas Gohr     * Aborts the given mode
81bd26d35bSAndreas Gohr     *
82bd26d35bSAndreas Gohr     * Aborting depends on the mode. It might unset certain input parameters or simply switch
83bd26d35bSAndreas Gohr     * the mode to something else (giving as return which needs to be passed back to the
84bd26d35bSAndreas Gohr     * ACTION_ACT_PREPROCESS event)
85bd26d35bSAndreas Gohr     *
86bd26d35bSAndreas Gohr     * @param string $act cleaned action mode
87bd26d35bSAndreas Gohr     * @return string the new mode to use
88bd26d35bSAndreas Gohr     */
891c08a51cSAndreas Gohr    protected function abort_action($act)
901c08a51cSAndreas Gohr    {
91bd26d35bSAndreas Gohr        global $INPUT;
92bd26d35bSAndreas Gohr
93bd26d35bSAndreas Gohr        switch ($act) {
94bd26d35bSAndreas Gohr            case 'save':
95bd26d35bSAndreas Gohr                return 'preview';
96bd26d35bSAndreas Gohr            case 'register':
97f74276b8SAndreas Gohr            case 'resendpwd':
98bd26d35bSAndreas Gohr                $INPUT->post->set('save', false);
99f74276b8SAndreas Gohr                return $act;
100643f15bdSAndreas Gohr            case 'login':
101643f15bdSAndreas Gohr                // we do not handle this here, but in handle_login()
102bd26d35bSAndreas Gohr            default:
103bd26d35bSAndreas Gohr                return $act;
104bd26d35bSAndreas Gohr        }
105bd26d35bSAndreas Gohr    }
106bd26d35bSAndreas Gohr
107bd26d35bSAndreas Gohr    /**
108969b14c4SAndreas Gohr     * Should a login CAPTCHA be used?
109969b14c4SAndreas Gohr     *
110969b14c4SAndreas Gohr     * @return bool
111969b14c4SAndreas Gohr     */
112969b14c4SAndreas Gohr    protected function protectLogin()
113969b14c4SAndreas Gohr    {
114969b14c4SAndreas Gohr        $config = (int)$this->getConf('loginprotect');
115969b14c4SAndreas Gohr        if ($config < 1) return false; // not wanted
116969b14c4SAndreas Gohr        if ($config === 1) return true; // always wanted
117969b14c4SAndreas Gohr        $count = (new IpCounter())->get();
118969b14c4SAndreas Gohr        return $count > 2; // only after 3 failed attempts
119969b14c4SAndreas Gohr    }
120969b14c4SAndreas Gohr
121969b14c4SAndreas Gohr    /**
122643f15bdSAndreas Gohr     * Handles CAPTCHA check in login
123643f15bdSAndreas Gohr     *
124643f15bdSAndreas Gohr     * Logins happen very early in the DokuWiki lifecycle, so we have to intercept them
125643f15bdSAndreas Gohr     * in their own event.
126643f15bdSAndreas Gohr     *
127643f15bdSAndreas Gohr     * @param Doku_Event $event
128643f15bdSAndreas Gohr     * @param $param
129643f15bdSAndreas Gohr     */
1301c08a51cSAndreas Gohr    public function handle_login(Doku_Event $event, $param)
1311c08a51cSAndreas Gohr    {
132643f15bdSAndreas Gohr        global $INPUT;
133969b14c4SAndreas Gohr        if (!$this->protectLogin()) return; // no protection wanted
134643f15bdSAndreas Gohr        if (!$INPUT->bool('u')) return; // this login was not triggered by a form
135643f15bdSAndreas Gohr
136643f15bdSAndreas Gohr        // we need to have $ID set for the captcha check
137643f15bdSAndreas Gohr        global $ID;
138643f15bdSAndreas Gohr        $ID = getID();
139643f15bdSAndreas Gohr
140643f15bdSAndreas Gohr        /** @var helper_plugin_captcha $helper */
141643f15bdSAndreas Gohr        $helper = plugin_load('helper', 'captcha');
142643f15bdSAndreas Gohr        if (!$helper->check()) {
143643f15bdSAndreas Gohr            $event->data['silent'] = true; // we have our own message
144643f15bdSAndreas Gohr            $event->result = false; // login fail
145643f15bdSAndreas Gohr            $event->preventDefault();
146643f15bdSAndreas Gohr            $event->stopPropagation();
147643f15bdSAndreas Gohr        }
148643f15bdSAndreas Gohr    }
149643f15bdSAndreas Gohr
150643f15bdSAndreas Gohr    /**
151643f15bdSAndreas Gohr     * Intercept all actions and check for CAPTCHA first.
15242a27035SAndreas Gohr     */
1531c08a51cSAndreas Gohr    public function handle_captcha_input(Doku_Event $event, $param)
1541c08a51cSAndreas Gohr    {
155*64382f29SAndreas Gohr        global $INPUT;
156*64382f29SAndreas Gohr
1577218f96cSAndreas Gohr        $act = act_clean($event->data);
158f74276b8SAndreas Gohr        if (!$this->needs_checking($act)) return;
15993f66506SAndreas Gohr
16042a27035SAndreas Gohr        // do nothing if logged in user and no CAPTCHA required
161*64382f29SAndreas Gohr        if (!$this->getConf('forusers') && $INPUT->server->str('REMOTE_USER')) {
16242a27035SAndreas Gohr            return;
16342a27035SAndreas Gohr        }
16442a27035SAndreas Gohr
16577e00bf9SAndreas Gohr        // check captcha
1667218f96cSAndreas Gohr        /** @var helper_plugin_captcha $helper */
16777e00bf9SAndreas Gohr        $helper = plugin_load('helper', 'captcha');
16877e00bf9SAndreas Gohr        if (!$helper->check()) {
169bd26d35bSAndreas Gohr            $event->data = $this->abort_action($act);
17042a27035SAndreas Gohr        }
17142a27035SAndreas Gohr    }
17242a27035SAndreas Gohr
17342a27035SAndreas Gohr    /**
1741c08a51cSAndreas Gohr     * Inject the CAPTCHA in a DokuForm or \dokuwiki\Form\Form
17542a27035SAndreas Gohr     */
1761c08a51cSAndreas Gohr    public function handle_form_output(Doku_Event $event, $param)
1771c08a51cSAndreas Gohr    {
178*64382f29SAndreas Gohr        global $INPUT;
179*64382f29SAndreas Gohr
180c0439b03SAndreas Gohr        if (
181c0439b03SAndreas Gohr            ($event->name === 'FORM_LOGIN_OUTPUT' || $event->name === 'HTML_LOGINFORM_OUTPUT')
182c0439b03SAndreas Gohr            &&
183c0439b03SAndreas Gohr            !$this->protectLogin()
184c0439b03SAndreas Gohr        ) {
185c0439b03SAndreas Gohr            // no login protection wanted
186c0439b03SAndreas Gohr            return;
187c0439b03SAndreas Gohr        }
18831c8e2bdSAndreas Gohr
1891c08a51cSAndreas Gohr        /** @var \dokuwiki\Form\Form|\Doku_Form $form */
1901c08a51cSAndreas Gohr        $form = $event->data;
1911c08a51cSAndreas Gohr
19247afabe6SAndreas Gohr        // get position of submit button
1931c08a51cSAndreas Gohr        if (is_a($form, \dokuwiki\Form\Form::class)) {
1941c08a51cSAndreas Gohr            $pos = $form->findPositionByAttribute('type', 'submit');
1951c08a51cSAndreas Gohr        } else {
1961c08a51cSAndreas Gohr            $pos = $form->findElementByAttribute('type', 'submit');
1971c08a51cSAndreas Gohr        }
19847afabe6SAndreas Gohr        if (!$pos) return; // no button -> source view mode
19947afabe6SAndreas Gohr
20042a27035SAndreas Gohr        // do nothing if logged in user and no CAPTCHA required
201*64382f29SAndreas Gohr        if (!$this->getConf('forusers') && $INPUT->server->str('REMOTE_USER')) {
20242a27035SAndreas Gohr            return;
20342a27035SAndreas Gohr        }
20442a27035SAndreas Gohr
20577e00bf9SAndreas Gohr        // get the CAPTCHA
2067218f96cSAndreas Gohr        /** @var helper_plugin_captcha $helper */
20777e00bf9SAndreas Gohr        $helper = plugin_load('helper', 'captcha');
20877e00bf9SAndreas Gohr        $out = $helper->getHTML();
20947afabe6SAndreas Gohr
2101c08a51cSAndreas Gohr        // insert before the submit button
2111c08a51cSAndreas Gohr        if (is_a($form, \dokuwiki\Form\Form::class)) {
2121c08a51cSAndreas Gohr            $form->addHTML($out, $pos);
2131c08a51cSAndreas Gohr        } else {
2141c08a51cSAndreas Gohr            $form->insertElement($pos, $out);
2151c08a51cSAndreas Gohr        }
21642a27035SAndreas Gohr    }
21742a27035SAndreas Gohr
218cde3ece1SAndreas Gohr    /**
219cde3ece1SAndreas Gohr     * Clean cookies once per day
220cde3ece1SAndreas Gohr     */
2211c08a51cSAndreas Gohr    public function handle_indexer(Doku_Event $event, $param)
2221c08a51cSAndreas Gohr    {
223cde3ece1SAndreas Gohr        $lastrun = getCacheName('captcha', '.captcha');
224cde3ece1SAndreas Gohr        $last = @filemtime($lastrun);
225cde3ece1SAndreas Gohr        if (time() - $last < 24 * 60 * 60) return;
226cde3ece1SAndreas Gohr
227cde3ece1SAndreas Gohr        /** @var helper_plugin_captcha $helper */
228cde3ece1SAndreas Gohr        $helper = plugin_load('helper', 'captcha');
229cde3ece1SAndreas Gohr        $helper->_cleanCaptchaCookies();
2305d59bd09SAndreas Gohr        touch($lastrun);
231cde3ece1SAndreas Gohr
232cde3ece1SAndreas Gohr        $event->preventDefault();
233cde3ece1SAndreas Gohr        $event->stopPropagation();
234cde3ece1SAndreas Gohr    }
235969b14c4SAndreas Gohr
236969b14c4SAndreas Gohr    /**
237969b14c4SAndreas Gohr     * Count failed login attempts
238969b14c4SAndreas Gohr     */
239969b14c4SAndreas Gohr    public function handle_auth(Doku_Event $event, $param)
240969b14c4SAndreas Gohr    {
241969b14c4SAndreas Gohr        global $INPUT;
242969b14c4SAndreas Gohr        $act = act_clean($event->data);
243969b14c4SAndreas Gohr        if (
244969b14c4SAndreas Gohr            $act != 'logout' &&
24531c8e2bdSAndreas Gohr            $INPUT->str('u') !== '' &&
246969b14c4SAndreas Gohr            empty($INPUT->server->str('http_credentials')) &&
247969b14c4SAndreas Gohr            empty($INPUT->server->str('REMOTE_USER'))
248969b14c4SAndreas Gohr        ) {
249969b14c4SAndreas Gohr            // This is a failed authentication attempt, count it
250969b14c4SAndreas Gohr            (new IpCounter())->increment();
251969b14c4SAndreas Gohr        }
252969b14c4SAndreas Gohr
253969b14c4SAndreas Gohr        if (
254969b14c4SAndreas Gohr            $act == 'login' &&
255969b14c4SAndreas Gohr            !empty($INPUT->server->str('REMOTE_USER'))
256969b14c4SAndreas Gohr        ) {
257969b14c4SAndreas Gohr            // This is a successful login, reset the counter
258969b14c4SAndreas Gohr            (new IpCounter())->reset();
259969b14c4SAndreas Gohr        }
260969b14c4SAndreas Gohr    }
26142a27035SAndreas Gohr}
26242a27035SAndreas Gohr
263