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