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