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 if ($this->protectLogin()) { 35 // inject in login form 36 $controller->register_hook('HTML_LOGINFORM_OUTPUT', 'BEFORE', $this, 'handle_form_output', []); // old 37 $controller->register_hook('FORM_LOGIN_OUTPUT', 'BEFORE', $this, 'handle_form_output', []); // new 38 39 // check on login 40 $controller->register_hook('AUTH_LOGIN_CHECK', 'BEFORE', $this, 'handle_login', []); 41 } 42 43 // clean up captcha cookies 44 $controller->register_hook('INDEXER_TASKS_RUN', 'AFTER', $this, 'handle_indexer', []); 45 46 $onk = $this->getConf('loginprotect'); 47 48 // log authentication failures 49 if ((int)$this->getConf('loginprotect') > 1) { 50 $controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'handle_auth', []); 51 } 52 } 53 54 /** 55 * Check if the current mode should be handled by CAPTCHA 56 * 57 * Note: checking needs to be done when a form has been submitted, not when the form 58 * is shown for the first time. Except for the editing process this is not determined 59 * by $act alone but needs to inspect other input variables. 60 * 61 * @param string $act cleaned action mode 62 * @return bool 63 */ 64 protected function needs_checking($act) 65 { 66 global $INPUT; 67 68 switch ($act) { 69 case 'save': 70 return true; 71 case 'register': 72 case 'resendpwd': 73 return $INPUT->bool('save'); 74 case 'login': 75 // we do not handle this here, but in handle_login() 76 default: 77 return false; 78 } 79 } 80 81 /** 82 * Aborts the given mode 83 * 84 * Aborting depends on the mode. It might unset certain input parameters or simply switch 85 * the mode to something else (giving as return which needs to be passed back to the 86 * ACTION_ACT_PREPROCESS event) 87 * 88 * @param string $act cleaned action mode 89 * @return string the new mode to use 90 */ 91 protected function abort_action($act) 92 { 93 global $INPUT; 94 95 switch ($act) { 96 case 'save': 97 return 'preview'; 98 case 'register': 99 case 'resendpwd': 100 $INPUT->post->set('save', false); 101 return $act; 102 case 'login': 103 // we do not handle this here, but in handle_login() 104 default: 105 return $act; 106 } 107 } 108 109 /** 110 * Should a login CAPTCHA be used? 111 * 112 * @return bool 113 */ 114 protected function protectLogin() 115 { 116 $config = (int)$this->getConf('loginprotect'); 117 if ($config < 1) return false; // not wanted 118 if ($config === 1) return true; // always wanted 119 $count = (new IpCounter())->get(); 120 return $count > 2; // only after 3 failed attempts 121 } 122 123 /** 124 * Handles CAPTCHA check in login 125 * 126 * Logins happen very early in the DokuWiki lifecycle, so we have to intercept them 127 * in their own event. 128 * 129 * @param Doku_Event $event 130 * @param $param 131 */ 132 public function handle_login(Doku_Event $event, $param) 133 { 134 global $INPUT; 135 if (!$this->protectLogin()) return; // no protection wanted 136 if (!$INPUT->bool('u')) return; // this login was not triggered by a form 137 138 // we need to have $ID set for the captcha check 139 global $ID; 140 $ID = getID(); 141 142 /** @var helper_plugin_captcha $helper */ 143 $helper = plugin_load('helper', 'captcha'); 144 if (!$helper->check()) { 145 $event->data['silent'] = true; // we have our own message 146 $event->result = false; // login fail 147 $event->preventDefault(); 148 $event->stopPropagation(); 149 } 150 } 151 152 /** 153 * Intercept all actions and check for CAPTCHA first. 154 */ 155 public function handle_captcha_input(Doku_Event $event, $param) 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') && $_SERVER['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 /** @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->has('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