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 $act = act_clean($event->data); 156 if (!$this->needs_checking($act)) return; 157 158 // do nothing if logged in user and no CAPTCHA required 159 if (!$this->getConf('forusers') && $_SERVER['REMOTE_USER']) { 160 return; 161 } 162 163 // check captcha 164 /** @var helper_plugin_captcha $helper */ 165 $helper = plugin_load('helper', 'captcha'); 166 if (!$helper->check()) { 167 $event->data = $this->abort_action($act); 168 } 169 } 170 171 /** 172 * Inject the CAPTCHA in a DokuForm or \dokuwiki\Form\Form 173 */ 174 public function handle_form_output(Doku_Event $event, $param) 175 { 176 if (!$this->protectLogin()) return; // no protection wanted 177 178 /** @var \dokuwiki\Form\Form|\Doku_Form $form */ 179 $form = $event->data; 180 181 // get position of submit button 182 if (is_a($form, \dokuwiki\Form\Form::class)) { 183 $pos = $form->findPositionByAttribute('type', 'submit'); 184 } else { 185 $pos = $form->findElementByAttribute('type', 'submit'); 186 } 187 if (!$pos) return; // no button -> source view mode 188 189 // do nothing if logged in user and no CAPTCHA required 190 if (!$this->getConf('forusers') && $_SERVER['REMOTE_USER']) { 191 return; 192 } 193 194 // get the CAPTCHA 195 /** @var helper_plugin_captcha $helper */ 196 $helper = plugin_load('helper', 'captcha'); 197 $out = $helper->getHTML(); 198 199 // insert before the submit button 200 if (is_a($form, \dokuwiki\Form\Form::class)) { 201 $form->addHTML($out, $pos); 202 } else { 203 $form->insertElement($pos, $out); 204 } 205 } 206 207 /** 208 * Clean cookies once per day 209 */ 210 public function handle_indexer(Doku_Event $event, $param) 211 { 212 $lastrun = getCacheName('captcha', '.captcha'); 213 $last = @filemtime($lastrun); 214 if (time() - $last < 24 * 60 * 60) return; 215 216 /** @var helper_plugin_captcha $helper */ 217 $helper = plugin_load('helper', 'captcha'); 218 $helper->_cleanCaptchaCookies(); 219 touch($lastrun); 220 221 $event->preventDefault(); 222 $event->stopPropagation(); 223 } 224 225 /** 226 * Count failed login attempts 227 */ 228 public function handle_auth(Doku_Event $event, $param) 229 { 230 global $INPUT; 231 $act = act_clean($event->data); 232 if ( 233 $act != 'logout' && 234 $INPUT->str('u') !== '' && 235 empty($INPUT->server->str('http_credentials')) && 236 empty($INPUT->server->str('REMOTE_USER')) 237 ) { 238 // This is a failed authentication attempt, count it 239 (new IpCounter())->increment(); 240 } 241 242 if ( 243 $act == 'login' && 244 !empty($INPUT->server->str('REMOTE_USER')) 245 ) { 246 // This is a successful login, reset the counter 247 (new IpCounter())->reset(); 248 } 249 } 250} 251 252