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