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