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